diff --git a/UNRELEASED.md b/UNRELEASED.md index 467ca90a654..314d1c90fa7 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -16,6 +16,16 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Bug fixes +- Fixed alignment of `Page` and `TopBar` so the search aligns to the page. ([#3610](https://github.com/Shopify/polaris-react/pull/3610)) +- Removed extra bottom border on the `DataTable` and added curved edges to footers ([#3571](https://github.com/Shopify/polaris-react/pull/3571)) +- **`Button`:** `loading` no longer sets the invalid `role="alert"` ([#3590](https://github.com/Shopify/polaris-react/pull/3590)) +- Removed `tabIndex=-1` from `Popover` when `preventAutoFocus` is true ([#3595](https://github.com/Shopify/polaris-react/pull/3595)) +- Fix `Filters` janky animation when opening and closing ([#3606](https://github.com/Shopify/polaris-react/pull/3606)) +- Fixed `Modal` header border color ([#3616](https://github.com/Shopify/polaris-react/pull/3616)) +- Fixed `TopBar` search clear button alignment on iOS ([#3618](https://github.com/Shopify/polaris-react/pull/3618)) +- Added dependency list to useImperativeHandle in `Banner` ([#3478](https://github.com/Shopify/polaris-react/pull/3478)) +- Internationalize `Badge` labels ([#3655](https://github.com/Shopify/polaris-react/pull/3655)) +- Aligned the `::before` 'indicator' to edge of container for `ActionList` ([#3619](https://github.com/Shopify/polaris-react/pull/3619)) - Fixed `FocusManager` from tracking inactive items that prevented trap focusing([#3630](https://github.com/Shopify/polaris-react/pull/3630)) - Added escape keybind to `Tooltip` ([#3627](https://github.com/Shopify/polaris-react/pull/3627)) - Removed extra bottom border on the `DataTable` and added curved edges to footers ([#3571](https://github.com/Shopify/polaris-react/pull/3571)) diff --git a/scripts/build-validate.js b/scripts/build-validate.js index 0347f61b370..181b5d9434d 100644 --- a/scripts/build-validate.js +++ b/scripts/build-validate.js @@ -58,15 +58,6 @@ function validateEsNextBuild() { assert.ok(jsContent.includes("import './Avatar.css';")); assert.ok(jsContent.includes('"Avatar": "Polaris-Avatar_z763p"')); assert.ok(jsContent.includes('"hidden": "Polaris-Avatar--hidden_riqie"')); - - assert.ok( - fs - .readFileSync( - './dist/esnext/components/Collapsible/Collapsible.tsx.esnext', - 'utf-8', - ) - .includes('class Collapsible'), - ); } function validateSassPublicApi() { diff --git a/src/components/Collapsible/Collapsible.scss b/src/components/Collapsible/Collapsible.scss index 4513cc64fef..7ea844051dc 100644 --- a/src/components/Collapsible/Collapsible.scss +++ b/src/components/Collapsible/Collapsible.scss @@ -1,33 +1,30 @@ @import '../../styles/common'; .Collapsible { - overflow: hidden; - max-height: 0; padding-top: 0; padding-bottom: 0; - opacity: 0; - will-change: opacity, max-height; -} - -.animating { - transition-property: opacity, max-height; - transition-duration: duration(slow); + height: 0; + overflow: hidden; + will-change: height; + transition-property: height; + transition-duration: duration(fast); transition-timing-function: easing(out); } .open { - opacity: 1; -} - -.fullyOpen { + height: auto; overflow: visible; } +// Stop children from being focused when aria-hidden +// .Collapsible[aria-hidden='true'] { +// display: none; +// } + .expandOnPrint { @include when-printing { - opacity: 1; // stylelint-disable-next-line declaration-no-important - max-height: none !important; + height: auto !important; overflow: visible; } } diff --git a/src/components/Collapsible/Collapsible.tsx b/src/components/Collapsible/Collapsible.tsx index 73e59633acf..d7faa08f41a 100644 --- a/src/components/Collapsible/Collapsible.tsx +++ b/src/components/Collapsible/Collapsible.tsx @@ -1,10 +1,4 @@ -import React, { - createContext, - createRef, - TransitionEvent, - Component, - ComponentClass, -} from 'react'; +import React, {useState, useRef, useEffect} from 'react'; import {classNames} from '../../utilities/css'; @@ -30,165 +24,76 @@ export interface CollapsibleProps { children?: React.ReactNode; } -type AnimationState = - | 'idle' - | 'measuring' - | 'closingStart' - | 'closing' - | 'openingStart' - | 'opening'; - -interface State { - height?: number | null; - animationState: AnimationState; - open: boolean; -} - -const ParentCollapsibleExpandingContext = createContext(false); - -class CollapsibleInner extends Component { - static contextType = ParentCollapsibleExpandingContext; - - static getDerivedStateFromProps( - {open: willOpen}: CollapsibleProps, - {open, animationState: prevAnimationState}: State, - ) { - let nextAnimationState = prevAnimationState; - if (open !== willOpen) { - nextAnimationState = 'measuring'; - } - - return { - animationState: nextAnimationState, - open: willOpen, - }; - } - - context!: React.ContextType; - - state: State = { - height: null, - animationState: 'idle', - // eslint-disable-next-line react/no-unused-state - open: this.props.open, +export function Collapsible({ + id, + expandOnPrint, + open, + transition, + children, +}: CollapsibleProps) { + const [height, setHeight] = useState(null); + const [isOpen, setIsOpen] = useState(open); + const collapisbleContainer = useRef(null); + + const wrapperClassName = classNames( + styles.Collapsible, + expandOnPrint && styles.expandOnPrint, + isOpen && styles.open, + height && styles.animating, + ); + + const collapsibleStyles = { + ...(transition && { + transitionDuration: `${transition.duration}`, + transitionTimingFunction: `${transition.timingFunction}`, + }), + ...(typeof height === 'number' && { + height: `${height}px`, + overflow: 'hidden', + }), }; - private node = createRef(); - private heightNode = createRef(); - - componentDidUpdate({open: wasOpen}: CollapsibleProps) { - const {animationState} = this.state; - const parentCollapsibleExpanding = this.context; - - if (parentCollapsibleExpanding && animationState !== 'idle') { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - animationState: 'idle', - }); + // When animation is complete clean up + const handleCompleteAnimation = () => { + setHeight(null); + setIsOpen(open); + }; + // Measure the child height for open and close + useEffect(() => { + if (open === isOpen || !collapisbleContainer.current) { return; } - requestAnimationFrame(() => { - const heightNode = this.heightNode.current; - switch (animationState) { - case 'idle': - break; - case 'measuring': - this.setState({ - animationState: wasOpen ? 'closingStart' : 'openingStart', - height: wasOpen && heightNode ? heightNode.scrollHeight : 0, - }); - break; - case 'closingStart': - this.setState({ - animationState: 'closing', - height: 0, - }); - break; - case 'openingStart': - this.setState({ - animationState: 'opening', - height: heightNode ? heightNode.scrollHeight : 0, - }); - } - }); - } - - render() { - const {id, expandOnPrint, open, children, transition} = this.props; - const {animationState, height} = this.state; - const parentCollapsibleExpanding = this.context; + setHeight(collapisbleContainer.current.scrollHeight); + }, [open, isOpen]); - const animating = animationState !== 'idle'; - - const wrapperClassName = classNames( - styles.Collapsible, - open && styles.open, - animating && styles.animating, - !animating && open && styles.fullyOpen, - expandOnPrint && styles.expandOnPrint, - ); - - const displayHeight = collapsibleHeight(open, animationState, height); - - const content = animating || open || expandOnPrint ? children : null; - - const transitionProperties = transition - ? { - transitionDuration: `${transition.duration}`, - transitionTimingFunction: `${transition.timingFunction}`, - } - : null; - - return ( - -
-
{content}
-
-
- ); - } - - private handleTransitionEnd = (event: TransitionEvent) => { - const {target} = event; - if (target === this.node.current) { - this.setState({animationState: 'idle', height: null}); + // If closing, set the height zero on the next render + useEffect(() => { + if (open || height === null || !collapisbleContainer.current) { + return; } - }; -} - -function collapsibleHeight( - open: boolean, - animationState: AnimationState, - height?: number | null, -) { - if (animationState === 'idle' && open) { - return open ? 'none' : undefined; - } - if (animationState === 'measuring') { - return open ? undefined : 'none'; - } + // If it is currently animating put it back to zero + if (height !== collapisbleContainer.current.scrollHeight) { + setHeight(0); + return; + } - return `${height || 0}px`; + getComputedStyle(collapisbleContainer.current).height; + setHeight(0); + }, [height, open]); + + return ( +
handleCompleteAnimation()} + ref={collapisbleContainer} + // aria-hidden={!open && !isOpen} + > + {children} +
+ ); } - -export const Collapsible = CollapsibleInner as ComponentClass< - CollapsibleProps -> & - typeof CollapsibleInner; diff --git a/src/components/Collapsible/README.md b/src/components/Collapsible/README.md index 00317ebc1a8..ad9b4f0bf54 100644 --- a/src/components/Collapsible/README.md +++ b/src/components/Collapsible/README.md @@ -57,9 +57,9 @@ Use for a basic “show more” interaction when you need to display more conten ```jsx function CollapsibleExample() { - const [active, setActive] = useState(true); + const [open, setOpen] = useState(false); - const handleToggle = useCallback(() => setActive((active) => !active), []); + const handleToggle = useCallback(() => setOpen((open) => !open), []); return (
@@ -67,15 +67,16 @@ function CollapsibleExample() { Your mailing list lets you contact customers or visitors who have @@ -90,6 +91,70 @@ function CollapsibleExample() { } ``` +### Nested collapsible + +When you have multiple collapsibles inside each other. This should be avoided as it causes the user to open multiple collapsible areas. + +```jsx +function NestedCollapsibleExample() { + const [open, setOpen] = useState(true); + const [innerOpen, setInnerOpen] = useState(false); + + const handleToggle = useCallback(() => setOpen((open) => !open), []); + const handleInnerToggle = useCallback( + () => setInnerOpen((open) => !open), + [], + ); + + return ( +
+ + + + + + Your mailing list lets you contact customers or visitors who have + shown an interest in your store. Reach out to them with exclusive + offers or updates about your products. + + + + + Your mailing list lets you contact customers or visitors who + have shown an interest in your store. Reach out to them with + exclusive offers or updates about your products. + + + + + +
+ ); +} +``` + ![Collapsible on Android](/public_images/components/Collapsible/android/default@2x.png) diff --git a/src/components/Collapsible/tests/Collapsible.test.tsx b/src/components/Collapsible/tests/Collapsible.test.tsx index 0f6dfa436c4..16700294d6c 100644 --- a/src/components/Collapsible/tests/Collapsible.test.tsx +++ b/src/components/Collapsible/tests/Collapsible.test.tsx @@ -17,22 +17,6 @@ describe('', () => { const hidden = collapsible.find(ariaHiddenSelector); expect(hidden.exists()).toBe(true); - expect(collapsible.contains('content')).toBe(false); - }); - - it('does not render its children when going from open to closed', () => { - const Child = () => null; - - const collapsible = mountWithAppProvider( - - - , - ); - - expect(collapsible.find(Child)).toHaveLength(1); - collapsible.setProps({open: false}); - collapsible.simulate('transitionEnd'); - expect(collapsible.find(Child)).toHaveLength(0); }); it('renders its children and does not render aria-hidden when open', () => {