diff --git a/packages/gamut-styles/src/variance/config.ts b/packages/gamut-styles/src/variance/config.ts index e179fdf74a8..6f14c7ee820 100644 --- a/packages/gamut-styles/src/variance/config.ts +++ b/packages/gamut-styles/src/variance/config.ts @@ -323,13 +323,56 @@ export const layout = { properties: ['width', 'height'], transform: transformSize, }, - width: { property: 'width', transform: transformSize }, - minWidth: { property: 'minWidth', transform: transformSize }, - maxWidth: { property: 'maxWidth', transform: transformSize }, - height: { property: 'height', transform: transformSize }, - minHeight: { property: 'minHeight', transform: transformSize }, - maxHeight: { property: 'maxHeight', transform: transformSize }, + width: { + property: { + physical: 'width', + logical: 'inlineSize', + }, + resolveProperty: getPropertyMode, + transform: transformSize, + }, + minWidth: { + property: { + physical: 'minWidth', + logical: 'minInlineSize', + }, + resolveProperty: getPropertyMode, + transform: transformSize, + }, + maxWidth: { + property: { + physical: 'maxWidth', + logical: 'maxInlineSize', + }, + resolveProperty: getPropertyMode, + transform: transformSize, + }, + height: { + property: { + physical: 'height', + logical: 'blockSize', + }, + resolveProperty: getPropertyMode, + transform: transformSize, + }, + minHeight: { + property: { + physical: 'minHeight', + logical: 'minBlockSize', + }, + resolveProperty: getPropertyMode, + transform: transformSize, + }, + maxHeight: { + property: { + physical: 'maxHeight', + logical: 'maxBlockSize', + }, + resolveProperty: getPropertyMode, + transform: transformSize, + }, verticalAlign: { property: 'verticalAlign' }, + direction: { property: 'direction' }, ...selfAlignments, ...gridItems, ...flexItems, diff --git a/packages/gamut/src/ContentContainer/__tests__/ContentContainer.test.tsx b/packages/gamut/src/ContentContainer/__tests__/ContentContainer.test.tsx index 6a152c66375..50660812888 100644 --- a/packages/gamut/src/ContentContainer/__tests__/ContentContainer.test.tsx +++ b/packages/gamut/src/ContentContainer/__tests__/ContentContainer.test.tsx @@ -6,11 +6,15 @@ import { ContentContainer } from '..'; const renderView = setupRtl(ContentContainer); describe('ContentContainer', () => { - it('has maxWidth of contentWidths.max when size is medium', () => { + // Note: Only testing one mode here since variant() caches styles after first render. + it('has maxInlineSize of contentWidths.max when size is medium', () => { + const useLogicalProperties = true; + const maxWidthProp = useLogicalProperties ? 'maxInlineSize' : 'maxWidth'; + const { view } = renderView({ size: 'medium' }); expect(view.container.firstChild).toHaveStyle( - `maxWidth: ${contentWidths.max}` + `${maxWidthProp}: ${contentWidths.max}` ); }); }); diff --git a/packages/gamut/src/DataList/__tests__/DataGrid.test.tsx b/packages/gamut/src/DataList/__tests__/DataGrid.test.tsx index 2d0904af37d..2bf0abe6c3a 100644 --- a/packages/gamut/src/DataList/__tests__/DataGrid.test.tsx +++ b/packages/gamut/src/DataList/__tests__/DataGrid.test.tsx @@ -1,6 +1,12 @@ -import { setupRtl } from '@codecademy/gamut-tests'; +import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests'; import { matchers } from '@emotion/jest'; -import { act, fireEvent, RenderResult, screen } from '@testing-library/react'; +import { + act, + fireEvent, + render, + RenderResult, + screen, +} from '@testing-library/react'; import { DataGrid, DataGridProps } from '../DataGrid'; import { ColumnConfig } from '../types'; @@ -583,37 +589,58 @@ describe('DataGrid', () => { }); }); - describe('wrapperWidth prop', () => { - it('applies wrapperWidth to the table container when provided', () => { - renderView({ wrapperWidth: '600px' }); + describe.each([ + { + useLogicalProperties: true, + widthProp: 'inlineSize', + maxWidthProp: 'maxInlineSize', + }, + { + useLogicalProperties: false, + widthProp: 'width', + maxWidthProp: 'maxWidth', + }, + ])( + 'wrapperWidth prop (useLogicalProperties: $useLogicalProperties)', + ({ useLogicalProperties, widthProp, maxWidthProp }) => { + const renderWithLogicalProps = (overrides?: Partial) => + render( + + + + ); - const tableContainer = screen.getByTestId('scrollable-test'); - expect(tableContainer).toHaveStyle({ - maxWidth: '600px', - width: '600px', + it('applies wrapperWidth to the table container when provided', () => { + renderWithLogicalProps({ wrapperWidth: '600px' }); + + const tableContainer = screen.getByTestId('scrollable-test'); + expect(tableContainer).toHaveStyle({ + [maxWidthProp]: '600px', + [widthProp]: '600px', + }); }); - }); - it('uses default width when wrapperWidth is not provided', () => { - renderView(); + it('uses default width when wrapperWidth is not provided', () => { + renderWithLogicalProps(); - const tableContainer = screen.getByTestId('scrollable-test'); - expect(tableContainer).toHaveStyle({ - maxWidth: '100%', - width: 'inherit', + const tableContainer = screen.getByTestId('scrollable-test'); + expect(tableContainer).toHaveStyle({ + [maxWidthProp]: '100%', + [widthProp]: 'inherit', + }); }); - }); - it('passes wrapperWidth through to the underlying List component', () => { - renderView({ wrapperWidth: '750px' }); + it('passes wrapperWidth through to the underlying List component', () => { + renderWithLogicalProps({ wrapperWidth: '750px' }); - const tableContainer = screen.getByTestId('scrollable-test'); - expect(tableContainer).toHaveStyle({ - maxWidth: '750px', - width: '750px', + const tableContainer = screen.getByTestId('scrollable-test'); + expect(tableContainer).toHaveStyle({ + [maxWidthProp]: '750px', + [widthProp]: '750px', + }); }); - }); - }); + } + ); describe('Container query control', () => { it('applies container query styles by default', () => { diff --git a/packages/gamut/src/List/__tests__/List.test.tsx b/packages/gamut/src/List/__tests__/List.test.tsx index a25cb5ed4a2..4a991dfe69d 100644 --- a/packages/gamut/src/List/__tests__/List.test.tsx +++ b/packages/gamut/src/List/__tests__/List.test.tsx @@ -1,8 +1,9 @@ import { theme } from '@codecademy/gamut-styles'; -import { setupRtl } from '@codecademy/gamut-tests'; +import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests'; import { matchers } from '@emotion/jest'; +import { render } from '@testing-library/react'; -import { List } from '../List'; +import { List, ListProps } from '../List'; import { ListCol } from '../ListCol'; import { ListRow } from '../ListRow'; @@ -131,28 +132,60 @@ describe('List', () => { expect(view.queryByText('Surprise!')).toBeNull(); }); - describe('wrapperWidth prop', () => { - it('applies wrapperWidth to the table container when provided', () => { - const { view } = renderView({ wrapperWidth: '500px' }); - - const tableContainer = view.container.querySelector( - '[data-testid="scrollable-list-el"]' + describe.each([ + { + useLogicalProperties: true, + widthProp: 'inlineSize', + maxWidthProp: 'maxInlineSize', + }, + { + useLogicalProperties: false, + widthProp: 'width', + maxWidthProp: 'maxWidth', + }, + ])( + 'wrapperWidth prop (useLogicalProperties: $useLogicalProperties)', + ({ useLogicalProperties, widthProp, maxWidthProp }) => { + const defaultChildren = ( + + Hello + ); - expect(tableContainer).toHaveStyle({ maxWidth: '500px', width: '500px' }); - }); - it('uses inherit width when wrapperWidth is not provided', () => { - const { view } = renderView(); + const renderWithLogicalProps = (overrides?: Partial) => + render( + + + {overrides?.children ?? defaultChildren} + + + ); + + it('applies wrapperWidth to the table container when provided', () => { + const view = renderWithLogicalProps({ wrapperWidth: '500px' }); + + const tableContainer = view.container.querySelector( + '[data-testid="scrollable-list-el"]' + ); + expect(tableContainer).toHaveStyle({ + [maxWidthProp]: '500px', + [widthProp]: '500px', + }); + }); + + it('uses inherit width when wrapperWidth is not provided', () => { + const view = renderWithLogicalProps(); - const tableContainer = view.container.querySelector( - '[data-testid="scrollable-list-el"]' - ); - expect(tableContainer).toHaveStyle({ - maxWidth: '100%', - width: 'inherit', + const tableContainer = view.container.querySelector( + '[data-testid="scrollable-list-el"]' + ); + expect(tableContainer).toHaveStyle({ + [maxWidthProp]: '100%', + [widthProp]: 'inherit', + }); }); - }); - }); + } + ); it('applies container query styles by default', () => { const { view } = renderView(); diff --git a/packages/gamut/src/Pagination/EllipsisButton.tsx b/packages/gamut/src/Pagination/EllipsisButton.tsx index ded5feec1ce..bb3e4f36939 100644 --- a/packages/gamut/src/Pagination/EllipsisButton.tsx +++ b/packages/gamut/src/Pagination/EllipsisButton.tsx @@ -6,7 +6,7 @@ import { wrapWithSlideAnimation } from './utils'; export type EllipsisButtonProps = PaginationButtonProps & { 'aria-label': string; - direction: 'back' | 'forward'; + buttonDirection: 'back' | 'forward'; }; const ellipsisButtonContents = { ellipsis: '•••', back: '«', forward: '»' }; @@ -14,14 +14,13 @@ const ellipsisButtonContents = { ellipsis: '•••', back: '«', forward: '» export const BaseEllipsisButton = forwardRef< ButtonBaseElements, EllipsisButtonProps - // eslint-disable-next-line react/prop-types ->(({ direction, showButton, ...props }, ref) => { +>(({ buttonDirection, showButton, ...props }, ref) => { const [contents, setContents] = useState(ellipsisButtonContents.ellipsis); return ( setContents(ellipsisButtonContents[direction])} + onMouseEnter={() => setContents(ellipsisButtonContents[buttonDirection])} onMouseLeave={() => setContents(ellipsisButtonContents.ellipsis)} {...props} ref={ref} diff --git a/packages/gamut/src/Pagination/PaginationButton.tsx b/packages/gamut/src/Pagination/PaginationButton.tsx index 4b32afb5178..266355515a9 100644 --- a/packages/gamut/src/Pagination/PaginationButton.tsx +++ b/packages/gamut/src/Pagination/PaginationButton.tsx @@ -39,7 +39,6 @@ export type PaginationButtonProps = ComponentProps< export const PaginationButton = forwardRef< ButtonBaseElements, PaginationButtonProps - // eslint-disable-next-line react/prop-types >( ( { diff --git a/packages/gamut/src/Pagination/__tests__/Pagination.test.tsx b/packages/gamut/src/Pagination/__tests__/Pagination.test.tsx index 9896fce28cc..7a5de345d58 100644 --- a/packages/gamut/src/Pagination/__tests__/Pagination.test.tsx +++ b/packages/gamut/src/Pagination/__tests__/Pagination.test.tsx @@ -13,7 +13,7 @@ const renderView = setupRtl(Pagination, { interface TestHelpersType { view: RenderResult; pageNumber?: number; - direction?: 'forward' | 'back'; + buttonDirection?: 'forward' | 'back'; } const getPage = ({ view, pageNumber }: TestHelpersType) => { @@ -31,8 +31,12 @@ const getForwardButton = ({ view }: TestHelpersType) => { const getJumpButtonCount = ({ view }: TestHelpersType) => view.getAllByLabelText(/(Jump )+/).length; -const getJumpButton = ({ view, pageNumber, direction }: TestHelpersType) => { - return view.getByLabelText(`Jump ${direction} to page ${pageNumber}`); +const getJumpButton = ({ + view, + pageNumber, + buttonDirection, +}: TestHelpersType) => { + return view.getByLabelText(`Jump ${buttonDirection} to page ${pageNumber}`); }; describe('Pagination', () => { @@ -181,7 +185,7 @@ describe('Pagination', () => { const forwardButton = getJumpButton({ view, pageNumber: 6, - direction: 'forward', + buttonDirection: 'forward', }); fireEvent.click(forwardButton); @@ -201,7 +205,7 @@ describe('Pagination', () => { const backButton = getJumpButton({ view, pageNumber: 6, - direction: 'back', + buttonDirection: 'back', }); fireEvent.click(backButton); @@ -217,7 +221,7 @@ describe('Pagination', () => { const backButton = getJumpButton({ view, pageNumber: 3, - direction: 'back', + buttonDirection: 'back', }); fireEvent.click(backButton); @@ -226,13 +230,45 @@ describe('Pagination', () => { const forwardButton = getJumpButton({ view, pageNumber: 8, - direction: 'forward', + buttonDirection: 'forward', }); fireEvent.click(forwardButton); expect(onChange).toHaveBeenCalledWith(8); }); + + it('shows arrow on ellipsis button hover', () => { + const { view } = renderView({ totalPages: 15, defaultPageNumber: 8 }); + + const jumpForwardButton = getJumpButton({ + view, + pageNumber: 13, + buttonDirection: 'forward', + }); + + expect(jumpForwardButton).toHaveTextContent('•••'); + + fireEvent.mouseEnter(jumpForwardButton); + expect(jumpForwardButton).toHaveTextContent('»'); + + fireEvent.mouseLeave(jumpForwardButton); + expect(jumpForwardButton).toHaveTextContent('•••'); + + const jumpBackButton = getJumpButton({ + view, + pageNumber: 3, + buttonDirection: 'back', + }); + + expect(jumpBackButton).toHaveTextContent('•••'); + + fireEvent.mouseEnter(jumpBackButton); + expect(jumpBackButton).toHaveTextContent('«'); + + fireEvent.mouseLeave(jumpBackButton); + expect(jumpBackButton).toHaveTextContent('•••'); + }); }); describe('when there is a pageNumber prop provided', () => { diff --git a/packages/gamut/src/Pagination/index.tsx b/packages/gamut/src/Pagination/index.tsx index 11334fac78b..00a323cd580 100644 --- a/packages/gamut/src/Pagination/index.tsx +++ b/packages/gamut/src/Pagination/index.tsx @@ -159,8 +159,8 @@ export const Pagination: React.FC = ({ <> = ({ = ({ <> = ({ /> { - it('uses percentage as width when no minimumPercent is provided', () => { - const { view } = renderView({ percent: 50 }); - - const progressBar = view.getByTestId('progress-bar-bar'); - expect(progressBar).toHaveStyle({ width: '50%' }); - }); - - it('uses percentage as width when it is greater than minimumPercent', () => { - const { view } = renderView({ minimumPercent: 25, percent: 50 }); - - const progressBar = view.getByTestId('progress-bar-bar'); - expect(progressBar).toHaveStyle({ width: '50%' }); - }); - - it('uses minimumPercentage as width when it is greater than percent', () => { - const { view } = renderView({ minimumPercent: 75, percent: 50 }); - - const progressBar = view.getByTestId('progress-bar-bar'); - expect(progressBar).toHaveStyle({ width: '75%' }); - }); + describe.each([ + { useLogicalProperties: true, widthProp: 'inlineSize' }, + { useLogicalProperties: false, widthProp: 'width' }, + ])( + 'width styles (useLogicalProperties: $useLogicalProperties)', + ({ useLogicalProperties, widthProp }) => { + const renderWithLogicalProps = (props: Partial = {}) => + render( + + + + ); + + it('uses percentage as width when no minimumPercent is provided', () => { + renderWithLogicalProps({ percent: 50 }); + + const progressBar = screen.getByTestId('progress-bar-bar'); + expect(progressBar).toHaveStyle({ [widthProp]: '50%' }); + }); + + it('uses percentage as width when it is greater than minimumPercent', () => { + renderWithLogicalProps({ minimumPercent: 25, percent: 50 }); + + const progressBar = screen.getByTestId('progress-bar-bar'); + expect(progressBar).toHaveStyle({ [widthProp]: '50%' }); + }); + + it('uses minimumPercentage as width when it is greater than percent', () => { + renderWithLogicalProps({ minimumPercent: 75, percent: 50 }); + + const progressBar = screen.getByTestId('progress-bar-bar'); + expect(progressBar).toHaveStyle({ [widthProp]: '75%' }); + }); + } + ); it('does not include percentage visually when size is small', () => { const { view } = renderView({ size: 'small' }); diff --git a/packages/styleguide/src/lib/Foundations/System/Props/Layout.mdx b/packages/styleguide/src/lib/Foundations/System/Props/Layout.mdx index 8c687a4dee5..7da0cf0179f 100644 --- a/packages/styleguide/src/lib/Foundations/System/Props/Layout.mdx +++ b/packages/styleguide/src/lib/Foundations/System/Props/Layout.mdx @@ -1,8 +1,9 @@ -import { Meta } from '@storybook/blocks'; +import { Canvas, Meta } from '@storybook/blocks'; import { AboutHeader, TokenTable } from '~styleguide/blocks'; import { defaultColumns, getPropRows } from '../../shared/elements'; +import * as LayoutStories from './Layout.stories'; export const parameters = { title: 'Layout', @@ -11,7 +12,7 @@ export const parameters = { status: 'updating', }; - + @@ -31,4 +32,14 @@ const LayoutExample = styled.div(system.layout); />; ``` +## Examples + +### Width + + + +### Direction + + + diff --git a/packages/styleguide/src/lib/Foundations/System/Props/Layout.stories.tsx b/packages/styleguide/src/lib/Foundations/System/Props/Layout.stories.tsx new file mode 100644 index 00000000000..4f9d1440601 --- /dev/null +++ b/packages/styleguide/src/lib/Foundations/System/Props/Layout.stories.tsx @@ -0,0 +1,47 @@ +import { Box, Markdown } from '@codecademy/gamut'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Foundations/System/Props/Layout', + component: Box, +}; + +export default meta; +type Story = StoryObj; + +export const WidthExample: Story = { + render: () => ( + + + This box has It + takes up half the width of its container. Inspect the box to see the + rendered CSS property. + + + ), +}; + +export const DirectionExample: Story = { + render: () => ( + + + + Left-to-right text direction + (default for English). + + + + + Right-to-left text direction + (used for Arabic, Hebrew, etc.). + + + + ), +}; diff --git a/packages/styleguide/src/lib/Foundations/shared/elements.tsx b/packages/styleguide/src/lib/Foundations/shared/elements.tsx index 4d345df50be..03c54ace3f6 100644 --- a/packages/styleguide/src/lib/Foundations/shared/elements.tsx +++ b/packages/styleguide/src/lib/Foundations/shared/elements.tsx @@ -485,8 +485,8 @@ const TRANSFORM_COLUMN = { size: 'fill', render: ({ transform, resolveProperty }: any) => ( <> - {transform && {transform?.name}} - {resolveProperty && {resolveProperty?.name}} + {transform && {transform.name}}{' '} + {resolveProperty && {resolveProperty.name}} ), };