-
-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP - New Accordion hooks and components
- Loading branch information
1 parent
696867a
commit c9f7cea
Showing
15 changed files
with
603 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import * as React from 'react'; | ||
import * as Accordion from '@base_ui/react/Accordion'; | ||
|
||
export default function App() { | ||
return ( | ||
<div className="App AccordionDemo"> | ||
<pre>Plain HTML</pre> | ||
<div role="region" className="MyAccordion-root" aria-label="My Accordion Component"> | ||
<div className="MyAccordion-section"> | ||
<h3 className="MyAccordion-heading"> | ||
<button | ||
type="button" | ||
aria-controls="Panel1" | ||
aria-expanded="true" | ||
className="MyAccordion-trigger" | ||
id="Trigger1" | ||
> | ||
Panel 1 | ||
</button> | ||
</h3> | ||
<div id="Panel1" className="MyAccordion-panel" role="region" aria-labelledby="Trigger1"> | ||
This the contents of Panel 1 | ||
</div> | ||
</div> | ||
|
||
<div className="MyAccordion-section"> | ||
<h3 className="MyAccordion-heading"> | ||
<button | ||
type="button" | ||
aria-controls="Panel2" | ||
aria-expanded="false" | ||
className="MyAccordion-trigger" | ||
id="Trigger2" | ||
> | ||
Panel 2 | ||
</button> | ||
</h3> | ||
<div id="Panel2" className="MyAccordion-panel" role="region" aria-labelledby="Trigger2"> | ||
This the contents of Panel 2 | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<br /> | ||
<br /> | ||
<hr /> | ||
<br /> | ||
<br /> | ||
|
||
<pre>Base UI</pre> | ||
|
||
<Accordion.Root className="MyAccordion-root"> | ||
<Accordion.Section className="MyAccordion-section"> | ||
<Accordion.Heading className="MyAccordion-heading"> | ||
<Accordion.Trigger className="MyAccordion-trigger">Trigger 1</Accordion.Trigger> | ||
</Accordion.Heading> | ||
<Accordion.Panel className="MyAccordion-panel"> | ||
This is the contents of Accordion.Panel 1 | ||
</Accordion.Panel> | ||
</Accordion.Section> | ||
|
||
<Accordion.Section className="MyAccordion-section"> | ||
<Accordion.Heading className="MyAccordion-heading"> | ||
<Accordion.Trigger className="MyAccordion-trigger">Trigger 2</Accordion.Trigger> | ||
</Accordion.Heading> | ||
<Accordion.Panel className="MyAccordion-panel"> | ||
This is the contents of Accordion.Panel 2 | ||
</Accordion.Panel> | ||
</Accordion.Section> | ||
</Accordion.Root> | ||
</div> | ||
); | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { BaseUIComponentProps } from '../../utils/types'; | ||
import { useComponentRenderer } from '../../utils/useComponentRenderer'; | ||
import type { AccordionSection } from '../Section/AccordionSection'; | ||
import { useAccordionSectionContext } from '../Section/AccordionSectionContext'; | ||
import { accordionStyleHookMapping } from '../Section/styleHooks'; | ||
|
||
const AccordionHeading = React.forwardRef(function AccordionHeading( | ||
props: AccordionHeading.Props, | ||
forwardedRef: React.ForwardedRef<HTMLHeadingElement>, | ||
) { | ||
const { render, className, ...otherProps } = props; | ||
|
||
const { ownerState } = useAccordionSectionContext(); | ||
|
||
const { renderElement } = useComponentRenderer({ | ||
render: render ?? 'h3', | ||
ownerState, | ||
className, | ||
ref: forwardedRef, | ||
extraProps: otherProps, | ||
customStyleHookMapping: accordionStyleHookMapping, | ||
}); | ||
|
||
return renderElement(); | ||
}); | ||
|
||
export { AccordionHeading }; | ||
|
||
export namespace AccordionHeading { | ||
export interface Props extends BaseUIComponentProps<'h3', AccordionSection.OwnerState> {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { BaseUIComponentProps } from '../../utils/types'; | ||
import { useComponentRenderer } from '../../utils/useComponentRenderer'; | ||
import { useCollapsibleContext } from '../../Collapsible/Root/CollapsibleContext'; | ||
import { useCollapsibleContent } from '../../Collapsible/Content/useCollapsibleContent'; | ||
import type { AccordionSection } from '../Section/AccordionSection'; | ||
import { useAccordionSectionContext } from '../Section/AccordionSectionContext'; | ||
import { accordionStyleHookMapping } from '../Section/styleHooks'; | ||
|
||
export const AccordionPanel = React.forwardRef(function AccordionPanel( | ||
props: AccordionPanel.Props, | ||
forwardedRef: React.ForwardedRef<HTMLButtonElement>, | ||
) { | ||
const { className, htmlHidden, render, ...otherProps } = props; | ||
|
||
const { animated, mounted, open, contentId, setContentId, setMounted, setOpen } = | ||
useCollapsibleContext(); | ||
|
||
const { getRootProps, height } = useCollapsibleContent({ | ||
animated, | ||
htmlHidden, | ||
id: contentId, | ||
mounted, | ||
open, | ||
ref: forwardedRef, | ||
setContentId, | ||
setMounted, | ||
setOpen, | ||
}); | ||
|
||
const { ownerState } = useAccordionSectionContext(); | ||
|
||
const { renderElement } = useComponentRenderer({ | ||
propGetter: getRootProps, | ||
render: render ?? 'div', | ||
ownerState, | ||
className, | ||
extraProps: { | ||
...otherProps, | ||
style: { | ||
'--accordion-content-height': height ? `${height}px` : undefined, | ||
}, | ||
}, | ||
customStyleHookMapping: accordionStyleHookMapping, | ||
}); | ||
|
||
return renderElement(); | ||
}); | ||
|
||
export namespace AccordionPanel { | ||
export interface Props | ||
extends BaseUIComponentProps<'div', AccordionSection.OwnerState>, | ||
Pick<useCollapsibleContent.Parameters, 'htmlHidden'> {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { FloatingList } from '@floating-ui/react'; | ||
import { BaseUIComponentProps } from '../../utils/types'; | ||
import { useComponentRenderer } from '../../utils/useComponentRenderer'; | ||
import { useAccordionRoot } from './useAccordionRoot'; | ||
import { AccordionRootContext } from './AccordionRootContext'; | ||
|
||
const AccordionRoot = React.forwardRef(function AccordionRoot( | ||
props: AccordionRoot.Props, | ||
forwardedRef: React.ForwardedRef<HTMLDivElement>, | ||
) { | ||
const { animated, disabled, defaultValue, value, className, render, ...otherProps } = props; | ||
|
||
const { getRootProps, ...accordion } = useAccordionRoot({ | ||
animated, | ||
disabled, | ||
defaultValue, | ||
value, | ||
}); | ||
|
||
const ownerState: AccordionRoot.OwnerState = React.useMemo( | ||
() => ({ | ||
value: accordion.value, | ||
disabled: accordion.disabled, | ||
// transitionStatus: accordion.transitionStatus, | ||
}), | ||
[accordion.value, accordion.disabled], | ||
); | ||
|
||
const contextValue: AccordionRoot.Context = React.useMemo( | ||
() => ({ | ||
...accordion, | ||
ownerState, | ||
}), | ||
[accordion, ownerState], | ||
); | ||
|
||
const { renderElement } = useComponentRenderer({ | ||
propGetter: getRootProps, | ||
render: render ?? 'div', | ||
className, | ||
ownerState, | ||
ref: forwardedRef, | ||
extraProps: otherProps, | ||
customStyleHookMapping: { | ||
value: () => null, | ||
}, | ||
}); | ||
|
||
return ( | ||
<AccordionRootContext.Provider value={contextValue}> | ||
<FloatingList elementsRef={accordion.accordionSectionRefs}>{renderElement()}</FloatingList> | ||
</AccordionRootContext.Provider> | ||
); | ||
}); | ||
|
||
export { AccordionRoot }; | ||
|
||
export namespace AccordionRoot { | ||
export interface Context extends Omit<useAccordionRoot.ReturnValue, 'getRootProps'> { | ||
ownerState: OwnerState; | ||
} | ||
|
||
export interface OwnerState { | ||
value: useAccordionRoot.Value; | ||
disabled: boolean; | ||
} | ||
|
||
export interface Props | ||
extends useAccordionRoot.Parameters, | ||
BaseUIComponentProps<any, OwnerState> {} | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import type { AccordionRoot } from './AccordionRoot'; | ||
|
||
/** | ||
* @ignore - internal component. | ||
*/ | ||
export const AccordionRootContext = React.createContext<AccordionRoot.Context | undefined>( | ||
undefined, | ||
); | ||
|
||
if (process.env.NODE_ENV !== 'production') { | ||
AccordionRootContext.displayName = 'AccordionRootContext'; | ||
} | ||
|
||
export function useAccordionRootContext() { | ||
const context = React.useContext(AccordionRootContext); | ||
if (context === undefined) { | ||
throw new Error('useAccordionRootContext must be used inside a Accordion component'); | ||
} | ||
return context; | ||
} |
Empty file.
107 changes: 107 additions & 0 deletions
107
packages/mui-base/src/Accordion/Root/useAccordionRoot.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { mergeReactProps } from '../../utils/mergeReactProps'; | ||
import { useControlled } from '../../utils/useControlled'; | ||
|
||
export function useAccordionRoot( | ||
parameters: useAccordionRoot.Parameters, | ||
): useAccordionRoot.ReturnValue { | ||
const { animated = true, defaultValue, value: valueParam, disabled = false } = parameters; | ||
|
||
const accordionSectionRefs = React.useRef<(HTMLElement | null)[]>([]); | ||
|
||
const [value, setValue] = useControlled({ | ||
controlled: valueParam, | ||
default: valueParam ?? defaultValue ?? [], | ||
name: 'Accordion', | ||
state: 'value', | ||
}); | ||
// console.log(value); | ||
|
||
const handleOpenChange = React.useCallback( | ||
(newValue: number | string, nextOpen: boolean) => { | ||
// console.group('useAccordionRoot handleOpenChange'); | ||
// console.log('newValue', newValue, 'nextOpen', nextOpen, 'openValues', value); | ||
if (nextOpen) { | ||
const nextOpenValues = value.slice(); | ||
nextOpenValues.push(newValue); | ||
setValue(nextOpenValues); | ||
} else { | ||
const nextOpenValues = value.filter((v) => v !== newValue); | ||
setValue(nextOpenValues); | ||
} | ||
// console.groupEnd(); | ||
}, | ||
[value], | ||
); | ||
|
||
const getRootProps = React.useCallback( | ||
(externalProps = {}) => | ||
mergeReactProps(externalProps, { | ||
role: 'region', | ||
}), | ||
[], | ||
); | ||
|
||
return React.useMemo( | ||
() => ({ | ||
getRootProps, | ||
accordionSectionRefs, | ||
animated, | ||
disabled, | ||
handleOpenChange, | ||
value, | ||
}), | ||
[getRootProps, accordionSectionRefs, animated, disabled, handleOpenChange, value], | ||
); | ||
} | ||
|
||
export namespace useAccordionRoot { | ||
export type Value = readonly (string | number)[]; | ||
|
||
export interface Parameters { | ||
/** | ||
* If `true`, the component supports CSS/JS-based animations and transitions. | ||
* @default true | ||
*/ | ||
animated?: boolean; | ||
/** | ||
* The value of the currently open `Accordion.Section` | ||
* This is the controlled counterpart of `defaultValue`. | ||
*/ | ||
value?: Value; | ||
/** | ||
* The default value representing the currently open `Accordion.Section` | ||
* This is the uncontrolled counterpart of `value`. | ||
* @default 0 | ||
*/ | ||
defaultValue?: Value; | ||
/** | ||
* Callback fired when an Accordion section is opened or closed. | ||
* The value representing the involved section is provided as an argument. | ||
*/ | ||
onOpenChange?: (value: Value) => void; | ||
/** | ||
* If `true`, the component is disabled. | ||
* @default false | ||
*/ | ||
disabled?: boolean; | ||
} | ||
|
||
export interface ReturnValue { | ||
getRootProps: ( | ||
externalProps?: React.ComponentPropsWithRef<'div'>, | ||
) => React.ComponentPropsWithRef<'div'>; | ||
accordionSectionRefs: React.MutableRefObject<(HTMLElement | null)[]>; | ||
animated: boolean; | ||
/** | ||
* The disabled state of the Accordion | ||
*/ | ||
disabled: boolean; | ||
handleOpenChange: (value: number | string, nextOpen: boolean) => void; | ||
/** | ||
* The open state of the Accordion | ||
*/ | ||
value: Value; | ||
} | ||
} |
Oops, something went wrong.