-
Notifications
You must be signed in to change notification settings - Fork 861
New useEuiContainerQuery hook
#9251
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
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
5bfd0e2
First version of useEuiContainerQuery
acstll 8efe389
[REVERT] Manual testing story
acstll 70f6203
Lint (missing license header)
acstll c5161cb
Update JSDoc
acstll 94bfadf
Lint (missing license header)
acstll 12cf3c1
Do not polyfill matchContainer, make it a util
acstll 6365bc2
Rename const, remove console.log
acstll bb85b9f
Better types?
acstll 39b5f8c
[matchContainer] HTMLElement type instead of Element
acstll 2d784ac
Add JSDoc and document matchContainer
acstll 3ab0cb9
[matchContainer] Export classes for testing, expose dispose method
acstll 73887ab
[matchContainer] Add cy test
acstll e8fe837
[useEuiContainerQuery] Add cy tests
acstll 21ccef9
Changelog
acstll 624bd0e
Add beta in JSDoc
acstll 94dc0f1
Revert "[REVERT] Manual testing story"
acstll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or 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 @@ | ||
| - Added `useEuiContainerQuery` hook to observe container query changes in JavaScript |
129 changes: 129 additions & 0 deletions
129
packages/eui/src/services/container_query/container_query_hook.spec.tsx
This file contains hidden or 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,129 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| /// <reference types="cypress" /> | ||
| /// <reference types="cypress-real-events" /> | ||
| /// <reference types="../../../cypress/support" /> | ||
|
|
||
| import React from 'react'; | ||
|
|
||
| import { useEuiContainerQuery } from './container_query_hook'; | ||
|
|
||
| const TestComponent = ({ | ||
| condition, | ||
| name, | ||
| }: { | ||
| condition: string; | ||
| name?: string; | ||
| }) => { | ||
| const { ref, matches } = useEuiContainerQuery<HTMLDivElement>( | ||
| condition, | ||
| name | ||
| ); | ||
|
|
||
| return ( | ||
| <div | ||
| data-test-subj="container" | ||
| style={ | ||
| { | ||
| container: `${name ?? 'none'} / inline-size`, | ||
| width: '400px', | ||
| } as React.CSSProperties | ||
| } | ||
| > | ||
| <div | ||
| ref={ref} | ||
| data-matches={matches} | ||
| data-test-subj="child" | ||
| style={{ outline: '1px solid lime' }} | ||
| > | ||
| Matches: {matches ? 'yes' : 'no'} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| describe('useEuiContainerQuery', () => { | ||
| it('matches according to container query', () => { | ||
| cy.realMount(<TestComponent condition="(width > 100px)" />); | ||
|
|
||
| cy.get('[data-test-subj="child"]') | ||
| .should('exist') | ||
| .invoke('attr', 'data-matches') | ||
| .should('eq', 'true'); | ||
|
|
||
| cy.get('[data-test-subj="container"]').should( | ||
| 'have.css', | ||
| 'container', | ||
| 'none / inline-size' | ||
| ); | ||
| }); | ||
|
|
||
| it('matches updates when container resizes', () => { | ||
| cy.mount(<TestComponent condition="(width > 250px)" />); | ||
|
|
||
| // initial state | ||
| cy.get('[data-test-subj="child"]') | ||
| .invoke('attr', 'data-matches') | ||
| .should('eq', 'true'); | ||
|
|
||
| // resize to smaller width | ||
| cy.get('[data-test-subj="container"]') | ||
| .invoke('css', 'width', '200px') | ||
| .wait(100); | ||
|
|
||
| cy.get('[data-test-subj="child"]') | ||
| .invoke('attr', 'data-matches') | ||
| .should('eq', 'false'); | ||
|
|
||
| // resize back to larger width | ||
| cy.get('[data-test-subj="container"]') | ||
| .invoke('css', 'width', '300px') | ||
| .wait(100); | ||
|
|
||
| cy.get('[data-test-subj="child"]') | ||
| .invoke('attr', 'data-matches') | ||
| .should('eq', 'true'); | ||
| }); | ||
|
|
||
| it('supports name in the container query', () => { | ||
| cy.mount( | ||
| <div | ||
| style={ | ||
| { | ||
| containerType: 'inline-size', | ||
| width: '200px', | ||
| outline: '1px solid cyan', | ||
| } as React.CSSProperties | ||
| } | ||
| > | ||
| <TestComponent condition="(width > 300px)" name="my-container" /> | ||
| </div> | ||
| ); | ||
|
|
||
| // targets the container named | ||
| cy.get('[data-test-subj="child"]') | ||
| .invoke('attr', 'data-matches') | ||
| .should('eq', 'true'); | ||
|
|
||
| // resize back to larger width | ||
| cy.get('[data-test-subj="container"]') | ||
| .invoke('css', 'width', '250px') | ||
| .wait(100); | ||
|
|
||
| cy.get('[data-test-subj="child"]') | ||
| .invoke('attr', 'data-matches') | ||
| .should('eq', 'false'); | ||
|
|
||
| cy.get('[data-test-subj="container"]').should( | ||
| 'have.css', | ||
| 'container', | ||
| 'my-container / inline-size' | ||
| ); | ||
| }); | ||
| }); |
62 changes: 62 additions & 0 deletions
62
packages/eui/src/services/container_query/container_query_hook.ts
This file contains hidden or 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,62 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| import { useEffect, useState, useRef } from 'react'; | ||
| import { matchContainer } from './match_container'; | ||
|
|
||
| /** | ||
| * React hook that subscribes to CSS container query changes. | ||
| * | ||
| * For this to work: | ||
| * - a proper container (an element with e.g. `container-type: inline-size`) is needed, and | ||
| * - the container MUST to be a parent of the element being observed | ||
| * | ||
| * @param containerCondition - A CSS `<container-condition>` string, e.g. `(width > 400px)` or `(min-width: 600px)` | ||
| * @param name - Optional container name, e.g. `sidebar` | ||
| * @returns An object containing: | ||
| * - `ref`: A ref to attach to the container element | ||
| * - `matches`: `true` if the container matches the condition, `false` otherwise | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * const { ref, matches } = useEuiContainerQuery('(width > 400px)'); | ||
| * return <div ref={ref}>{matches ? 'Wide' : 'Narrow'}</div>; | ||
| * ``` | ||
| * | ||
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@container | MDN: @container} | ||
| * @beta | ||
| */ | ||
| export function useEuiContainerQuery<T extends HTMLElement = HTMLElement>( | ||
| containerCondition: string, | ||
| name?: string | ||
tkajtoch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) { | ||
| const containerQueryString = name | ||
| ? `${name} ${containerCondition}` | ||
| : containerCondition; | ||
| const ref = useRef<T | null>(null); | ||
| const [matches, setMatches] = useState<boolean>(false); | ||
|
|
||
| useEffect(() => { | ||
| if (!ref.current) return; | ||
|
|
||
| const queryList = matchContainer(ref.current, containerQueryString); | ||
| const handleChange = () => { | ||
| setMatches(queryList.matches); | ||
| }; | ||
|
|
||
| setMatches(queryList.matches); | ||
| queryList.addEventListener('change', handleChange); | ||
|
|
||
| return () => { | ||
| queryList.removeEventListener('change', handleChange); | ||
| queryList.dispose(); | ||
| }; | ||
| }, [ref, containerQueryString]); | ||
|
|
||
| return { ref, matches }; | ||
| } | ||
This file contains hidden or 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,9 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| export * from './container_query_hook'; |
159 changes: 159 additions & 0 deletions
159
packages/eui/src/services/container_query/match_container.spec.tsx
This file contains hidden or 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,159 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| /// <reference types="cypress" /> | ||
| /// <reference types="cypress-real-events" /> | ||
| /// <reference types="../../../cypress/support" /> | ||
|
|
||
| import { | ||
| matchContainer, | ||
| ContainerQueryList, | ||
| ContainerQueryListChangeEvent, | ||
| } from './match_container'; | ||
|
|
||
| describe('matchContainer', () => { | ||
| before(() => { | ||
| cy.document().then((doc) => { | ||
| // create container element | ||
| const parent = doc.createElement('div'); | ||
| parent.setAttribute('data-test-subj', 'container'); | ||
| parent.style.containerType = 'inline-size'; | ||
|
|
||
| // create child | ||
| const child = doc.createElement('div'); | ||
| child.setAttribute('data-test-subj', 'child'); | ||
| child.textContent = 'Child element text'; | ||
| child.style.outline = '1px solid cyan'; | ||
| parent.appendChild(child); | ||
|
|
||
| doc.body.appendChild(parent); | ||
| }); | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| cy.get('[data-test-subj="container"]') | ||
| .should('exist') | ||
| .as('container') | ||
| .invoke('css', 'width', '250px'); | ||
| cy.get('[data-test-subj="child"]').should('exist').as('child'); | ||
| }); | ||
|
|
||
| it('returns a ContainerQueryList instance', () => { | ||
| const condition = '(width > 100px)'; | ||
|
|
||
| cy.get('@child').then(($child) => { | ||
| const cql = matchContainer($child[0], condition); | ||
| expect(cql).to.be.instanceOf(ContainerQueryList); | ||
| expect(cql).to.have.property('matches'); | ||
| expect(cql).to.have.property('container'); | ||
| expect(cql.container).to.equal(condition); | ||
| cql.dispose(); | ||
| }); | ||
| }); | ||
|
|
||
| it('matches is true when container query condition is met', () => { | ||
| cy.get('@child').then(($child) => { | ||
| const cql = matchContainer($child[0], '(width > 100px)'); | ||
| expect(cql.matches).to.equal(true); | ||
| cql.dispose(); | ||
| }); | ||
| }); | ||
|
|
||
| it('matches is false when container query condition is not met', () => { | ||
| cy.get('@child').then(($child) => { | ||
| const cql = matchContainer($child[0], '(width > 1000px)'); | ||
| expect(cql.matches).to.equal(false); | ||
| cql.dispose(); | ||
| }); | ||
| }); | ||
|
|
||
| it('matches is updated when DOM changes', () => { | ||
| cy.get('@child').then(($child) => { | ||
| const cql = matchContainer($child[0], '(width > 200px)'); | ||
| expect(cql.matches).to.equal(true); | ||
|
|
||
| cy.get('@container') | ||
| .invoke('css', 'width', '100px') | ||
| .wait(100) | ||
| .then(() => { | ||
| expect(cql.matches).to.equal(false); | ||
| cql.dispose(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| it('fires change event when matches changes', () => { | ||
| const changeSpy = cy.spy().as('changeSpy'); | ||
| cy.get('@child').then(($child) => { | ||
| const cql = matchContainer($child[0], '(width <= 250px)'); | ||
| cql.addEventListener('change', changeSpy); | ||
| expect(cql.matches).to.equal(true); | ||
|
|
||
| cy.get('[data-test-subj="container"]') | ||
| .invoke('css', 'width', '300px') | ||
| .wait(100) | ||
| .then(() => { | ||
| expect(changeSpy).to.have.been.calledOnce; | ||
| expect(changeSpy.firstCall.args[0]).to.be.instanceOf( | ||
| ContainerQueryListChangeEvent | ||
| ); | ||
| expect(cql.matches).to.equal(false); | ||
| }) | ||
| .invoke('css', 'width', '100px') | ||
| .wait(100) | ||
| .then(() => { | ||
| expect(changeSpy).to.have.been.calledTwice; | ||
| expect(cql.matches).to.equal(true); | ||
| cql.dispose(); | ||
| }) | ||
| .invoke('css', 'width', '400px') | ||
| .wait(100) | ||
| .then(() => { | ||
| // no more after cql.dispose() above | ||
| expect(changeSpy).to.have.been.calledTwice; | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| it('cleans up properly when calling dispose', () => { | ||
| cy.get('@child').then(($child) => { | ||
| const child = $child[0]; | ||
| const cql = matchContainer(child, '(width > 100px)'); | ||
| expect(cql.matches).to.equal(true); | ||
|
|
||
| // verify setup happened | ||
| const hasMarkerBefore = Array.from(child.attributes).some((attr) => | ||
| attr.name.startsWith('data-container-query-observer-') | ||
| ); | ||
| expect(hasMarkerBefore).to.equal(true); | ||
| expect(child.style.getPropertyValue('transition')).to.not.equal(''); | ||
| expect(child.style.getPropertyValue('transition-behavior')).to.equal( | ||
| 'allow-discrete' | ||
| ); | ||
|
|
||
| cy.document().then((doc) => { | ||
| expect(doc.adoptedStyleSheets.length).to.be.greaterThan(0); | ||
|
|
||
| // cleanup happens here | ||
| cql.dispose(); | ||
|
|
||
| expect(doc.adoptedStyleSheets.length).to.equal(0); | ||
|
|
||
| const hasMarkerAfter = Array.from(child.attributes).some((attr) => | ||
| attr.name.startsWith('data-container-query-observer-') | ||
| ); | ||
| expect(hasMarkerAfter).to.equal(false); | ||
|
|
||
| expect(child.style.getPropertyValue('transition')).to.equal(''); | ||
| expect(child.style.getPropertyValue('transition-behavior')).to.equal( | ||
| '' | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.