diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c0b31d3..ce44acd78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [2.36.2](https://github.com/EightfoldAI/octuple/compare/v2.36.1...v2.36.2) (2023-05-09) + +### Bug Fixes + +- stepper: handle scrollIntoView for both layouts on mount ([#613](https://github.com/EightfoldAI/octuple/issues/613)) ([209d5c3](https://github.com/EightfoldAI/octuple/commits/209d5c3cd492991e67cd854632cdf00193b32b6c)) + +### [2.36.1](https://github.com/EightfoldAI/octuple/compare/v2.36.0...v2.36.1) (2023-05-04) + +### Bug Fixes + +- select: ensure aria attributes target the intended element ([#612](https://github.com/EightfoldAI/octuple/issues/612)) ([387158d](https://github.com/EightfoldAI/octuple/commits/387158d892270c8317eb7a4dab4dfc5c0473398e)) + +## [2.36.0](https://github.com/EightfoldAI/octuple/compare/v2.35.3...v2.36.0) (2023-05-03) + +### Features + +- adding style option for Empty description ([#606](https://github.com/EightfoldAI/octuple/issues/606)) ([0539a61](https://github.com/EightfoldAI/octuple/commits/0539a61868990ba4e636a559b2ad919af72424a1)) +- linkbutton: add link button component ([#607](https://github.com/EightfoldAI/octuple/issues/607)) ([8b3d782](https://github.com/EightfoldAI/octuple/commits/8b3d782e20146413b623159ad280aaeb3360aed4)) + +### Bug Fixes + +- dropdown: use id from clone element props if present ([#608](https://github.com/EightfoldAI/octuple/issues/608)) ([6db0dc9](https://github.com/EightfoldAI/octuple/commits/6db0dc90eb628091c45e03cfa7c7d624b06a312c)) +- pagination: Issues with VisiblePagerCountSize ([#610](https://github.com/EightfoldAI/octuple/issues/610)) ([919c7c5](https://github.com/EightfoldAI/octuple/commits/919c7c5248a700a8e866ce2eeeac83ae3e9107b1)) + ### [2.35.3](https://github.com/EightfoldAI/octuple/compare/v2.35.2...v2.35.3) (2023-04-24) ### Bug Fixes diff --git a/package.json b/package.json index 6543650e4..1c9d37a8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eightfold.ai/octuple", - "version": "2.35.3", + "version": "2.36.2", "license": "MIT", "main": "lib/octuple.js", "types": "lib/octuple.d.ts", diff --git a/src/components/Button/DefaultButton/DefaultButton.tsx b/src/components/Button/DefaultButton/DefaultButton.tsx index f6e91ef35..f54501220 100644 --- a/src/components/Button/DefaultButton/DefaultButton.tsx +++ b/src/components/Button/DefaultButton/DefaultButton.tsx @@ -54,6 +54,7 @@ export const DefaultButton: FC = React.forwardRef( classNames={classNames} counter={counter} disabled={disabled} + disruptive={disruptive} dropShadow={dropShadow} floatingButtonProps={floatingButtonProps} htmlType={htmlType} diff --git a/src/components/Button/SystemUIButton/SystemUIButton.tsx b/src/components/Button/SystemUIButton/SystemUIButton.tsx index db7018049..5fbeb2a76 100644 --- a/src/components/Button/SystemUIButton/SystemUIButton.tsx +++ b/src/components/Button/SystemUIButton/SystemUIButton.tsx @@ -68,6 +68,7 @@ export const SystemUIButton: FC = React.forwardRef( style={style} text={text} toggle={toggle} + transparent={transparent} variant={ButtonVariant.SystemUI} /> ); diff --git a/src/components/Dropdown/Dropdown.test.tsx b/src/components/Dropdown/Dropdown.test.tsx index 563fe4fd9..910189657 100644 --- a/src/components/Dropdown/Dropdown.test.tsx +++ b/src/components/Dropdown/Dropdown.test.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import Enzyme from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import MatchMediaMock from 'jest-matchmedia-mock'; @@ -9,8 +9,10 @@ import { ButtonWidth, DefaultButton, } from '../Button'; -import { IconName } from '../Icon'; +import { Icon, IconName } from '../Icon'; import { List } from '../List'; +import { Stack } from '../Stack'; +import { TextInput } from '../Inputs'; import { render, screen, waitFor } from '@testing-library/react'; Enzyme.configure({ adapter: new Adapter() }); @@ -46,26 +48,26 @@ const Overlay = () => ( /> ); +const dropdownProps: object = { + trigger: 'click', + classNames: 'my-dropdown-class', + style: {}, + dropdownClassNames: 'my-dropdown-class', + dropdownStyle: { + color: 'red', + }, + placement: 'bottom-start', + overlay: Overlay(), + offset: 0, + positionStrategy: 'absolute', + disabled: false, + closeOnDropdownClick: true, + portal: false, +}; + const DropdownComponent = (): JSX.Element => { const [visible, setVisibility] = useState(false); - const dropdownProps: object = { - trigger: 'click', - classNames: 'my-dropdown-class', - style: {}, - dropdownClassNames: 'my-dropdown-class', - dropdownStyle: { - color: 'red', - }, - placement: 'bottom-start', - overlay: Overlay(), - offset: 0, - positionStrategy: 'absolute', - disabled: false, - closeOnDropdownClick: true, - portal: false, - }; - return ( { ); }; +const ComplexDropdownComponent = (): JSX.Element => { + const inputRef: React.MutableRefObject = + useRef(null); + const [visible, setVisibility] = useState(false); + + return ( + setVisibility(isVisible)} + > +
+ + + + +
+
+ ); +}; + describe('Dropdown', () => { beforeAll(() => { matchMedia = new MatchMediaMock(); @@ -132,4 +164,21 @@ describe('Dropdown', () => { const dropdownButton = screen.getByRole('button'); expect(dropdownButton.id).toBe('test-button-id'); }); + + test('Should support ariaRef prop for complex dropdown references', async () => { + const { container } = render(); + const dropdownAriaRef = screen.getByTestId('test-input-id'); + expect(dropdownAriaRef.getAttribute('aria-controls')).toBeTruthy(); + expect(dropdownAriaRef.getAttribute('aria-expanded')).toBe('false'); + expect(dropdownAriaRef.getAttribute('aria-haspopup')).toBe('true'); + expect(dropdownAriaRef.getAttribute('role')).toBe('combobox'); + dropdownAriaRef.click(); + await waitFor(() => screen.getByText('User profile 1')); + const option1 = screen.getByText('User profile 1'); + expect(option1).toBeTruthy(); + expect(container.querySelector('.dropdown-wrapper')?.classList).toContain( + 'my-dropdown-class' + ); + expect(dropdownAriaRef.getAttribute('aria-expanded')).toBe('true'); + }); }); diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index cd16629b4..efa8b2073 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -40,6 +40,7 @@ export const Dropdown: FC = React.memo( React.forwardRef( ( { + ariaRef, children, classNames, closeOnDropdownClick = true, @@ -243,6 +244,27 @@ export const Dropdown: FC = React.memo( if (child.props.id && dropdownReferenceId !== child.props.id) { setReferenceElementId(child.props.id); } + // If there's an ariaRef, apply the a11y attributes to it, rather than the immediate child. + if (ariaRef && ariaRef.current) { + ariaRef.current.setAttribute('aria-controls', dropdownId); + ariaRef.current.setAttribute('aria-expanded', `${mergedVisible}`); + ariaRef.current.setAttribute('aria-haspopup', 'true'); + + if (!ariaRef.current.hasAttribute('role')) { + ariaRef.current.setAttribute('role', 'button'); + } + + return cloneElement(child, { + ...{ + [TRIGGER_TO_HANDLER_MAP_ON_ENTER[trigger]]: toggle(true), + }, + id: dropdownReferenceId, + onClick: handleReferenceClick, + onKeyDown: handleReferenceKeyDown, + className: referenceWrapperClassNames, + }); + } + return cloneElement(child, { ...{ [TRIGGER_TO_HANDLER_MAP_ON_ENTER[trigger]]: toggle(true), diff --git a/src/components/Dropdown/Dropdown.types.ts b/src/components/Dropdown/Dropdown.types.ts index 276f2e28f..74cf16c91 100644 --- a/src/components/Dropdown/Dropdown.types.ts +++ b/src/components/Dropdown/Dropdown.types.ts @@ -16,6 +16,12 @@ export const TRIGGER_TO_HANDLER_MAP_ON_LEAVE = { }; export interface DropdownProps { + /** + * The ref of element that should implement the following props: + * 'aria-controls', 'aria-expanded', 'aria-haspopup', 'role' + * @default child + */ + ariaRef?: React.MutableRefObject; /** * Class names of the main wrapper */ diff --git a/src/components/Pagination/Pager.tsx b/src/components/Pagination/Pager.tsx index f9f5dda1f..37a39fce8 100644 --- a/src/components/Pagination/Pager.tsx +++ b/src/components/Pagination/Pager.tsx @@ -1,6 +1,7 @@ import React, { FC, useEffect, useCallback, useState, Ref } from 'react'; import { PagerProps, + PagerSizeOptions, PaginationVisiblePagerCountSizeOptions, } from './Pagination.types'; import { ButtonShape, ButtonSize, NeutralButton } from '../Button'; @@ -9,12 +10,6 @@ import { mergeClasses } from '../../shared/utilities'; import styles from './pagination.module.scss'; -/** Represents the number of pages from each edge of the list before we show quick buttons. */ -const EDGE_BUFFER_THRESHOLD: number = 5; - -/** Represents a list too short to display meaningful quick buttons. */ -const SHORT_LIST_THRESHOLD: number = 10; - /** Represents the number of list items (pages) visible at any given time. */ const VISIBLE_PAGER_COUNT = { [PaginationVisiblePagerCountSizeOptions.Small]: 3, @@ -22,6 +17,11 @@ const VISIBLE_PAGER_COUNT = { [PaginationVisiblePagerCountSizeOptions.Large]: 7, }; +const PAGER_SIZE_COUNT = { + [PagerSizeOptions.Small]: 5, + [PagerSizeOptions.Medium]: 7, +}; + export const Pager: FC = React.forwardRef( ( { @@ -33,17 +33,32 @@ export const Pager: FC = React.forwardRef( quickPreviousIconButtonAriaLabel, simplified = false, showLast = true, + pagerSize, visiblePagerCountSize = PaginationVisiblePagerCountSizeOptions.Large, ...rest }, ref: Ref ) => { + /** Represents the number of pages from each edge of the list before we show quick buttons. */ + let edgeBufferThreshold: number = 5; + /** Represents a list too short to display meaningful quick buttons. */ + let shortListThreshold: number = 10; + + if (pagerSize) { + edgeBufferThreshold = PAGER_SIZE_COUNT[pagerSize] - 2; + shortListThreshold = PAGER_SIZE_COUNT[pagerSize] + 3; + } const [_pagers, setPagers] = useState([0]); const [_quickNextActive, setQuickNextActive] = useState(false); const [_quickPreviousActive, setQuickPreviousActive] = useState(false); - const visiblePagerCount = VISIBLE_PAGER_COUNT?.[visiblePagerCountSize] || 7; + // TODO: Remove in Octuple v3.0.0 in favor of pagerSize prop + let visiblePagerCount = VISIBLE_PAGER_COUNT?.[visiblePagerCountSize] || 7; + + if (pagerSize) { + visiblePagerCount = PAGER_SIZE_COUNT?.[pagerSize] || 7; + } /** * Updates the visible range of pages in the UL based upon list @@ -58,7 +73,7 @@ export const Pager: FC = React.forwardRef( * The pageCount is greater than 10. * Keeping the quick buttons meaningful. */ - if (pageCount > SHORT_LIST_THRESHOLD) { + if (pageCount > shortListThreshold) { /** * The visible items in the array use a basic threshold. * e.g. [1 << 4 5 [6] 7 8 >> 100] where 6 is the currentPage @@ -67,9 +82,9 @@ export const Pager: FC = React.forwardRef( * the 5 items between the two quick buttons they must represent * the correct position in the array. */ - const afterQuickPrevious: boolean = currentPage > EDGE_BUFFER_THRESHOLD; + const afterQuickPrevious: boolean = currentPage > edgeBufferThreshold; const beforeQuickNext: boolean = - currentPage < pageCount - EDGE_BUFFER_THRESHOLD; + currentPage < pageCount - edgeBufferThreshold; /** * The currentPage number is greater than the quick previous @@ -164,8 +179,8 @@ export const Pager: FC = React.forwardRef( )} {!simplified && - currentPage > EDGE_BUFFER_THRESHOLD && - pageCount > SHORT_LIST_THRESHOLD && ( + currentPage > edgeBufferThreshold && + pageCount > shortListThreshold && (
  • = React.forwardRef( onMouseEnter={() => setQuickPreviousActive(true)} onMouseLeave={() => setQuickPreviousActive(false)} onClick={() => - onCurrentChange(currentPage - EDGE_BUFFER_THRESHOLD) + onCurrentChange(currentPage - edgeBufferThreshold) } size={ButtonSize.Medium} /> @@ -212,8 +227,8 @@ export const Pager: FC = React.forwardRef( ); })} {!simplified && - currentPage < pageCount - EDGE_BUFFER_THRESHOLD && - pageCount > SHORT_LIST_THRESHOLD && ( + currentPage < pageCount - edgeBufferThreshold && + pageCount > shortListThreshold && (
  • = React.forwardRef( onMouseEnter={() => setQuickNextActive(true)} onMouseLeave={() => setQuickNextActive(false)} onClick={() => - onCurrentChange(currentPage + EDGE_BUFFER_THRESHOLD) + onCurrentChange(currentPage + edgeBufferThreshold) } size={ButtonSize.Medium} /> diff --git a/src/components/Pagination/Pagination.stories.tsx b/src/components/Pagination/Pagination.stories.tsx index 15341068a..a9e57aa02 100644 --- a/src/components/Pagination/Pagination.stories.tsx +++ b/src/components/Pagination/Pagination.stories.tsx @@ -1,11 +1,7 @@ import React from 'react'; import { Stories } from '@storybook/addon-docs'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { - Pagination, - PaginationLayoutOptions, - PaginationVisiblePagerCountSizeOptions, -} from './index'; +import { PagerSizeOptions, Pagination, PaginationLayoutOptions } from './index'; export default { title: 'Pagination', @@ -116,7 +112,7 @@ const paginationArgs: Object = { restrictPageSizesPropToSizesLayout: false, hideWhenSinglePage: false, total: 50, - visiblePagerCountSize: PaginationVisiblePagerCountSizeOptions.Large, + pagerSize: PagerSizeOptions.Medium, 'data-test-id': 'myPaginationTestId', }; diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index eabc01bbf..2953eb706 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -45,6 +45,8 @@ export const Pagination: FC = React.forwardRef( onCurrentChange, onSizeChange, pageCount, + // TODO: Set a default once visiblePagerCountSize is removed + pagerSize, pageSize = 10, pageSizeButtonAriaLabel: defaultPageSizeButtonAriaLabel, pageSizes = [10, 20, 30, 40, 50, 100], @@ -393,6 +395,7 @@ export const Pagination: FC = React.forwardRef( locale={locale} onCurrentChange={handleCurrentChange} pageCount={getPageCount()} + pagerSize={pagerSize} quickNextIconButtonAriaLabel={ quickNextIconButtonAriaLabel } @@ -412,6 +415,7 @@ export const Pagination: FC = React.forwardRef( locale={locale} onCurrentChange={handleCurrentChange} pageCount={getPageCount()} + pagerSize={pagerSize} quickNextIconButtonAriaLabel={ quickNextIconButtonAriaLabel } diff --git a/src/components/Pagination/Pagination.types.ts b/src/components/Pagination/Pagination.types.ts index 75a687db7..a0c749a31 100644 --- a/src/components/Pagination/Pagination.types.ts +++ b/src/components/Pagination/Pagination.types.ts @@ -60,6 +60,11 @@ export enum PaginationVisiblePagerCountSizeOptions { Large = 'large', } +export enum PagerSizeOptions { + Small = 'small', + Medium = 'medium', +} + export type PaginationLocale = { lang: Locale; }; @@ -131,6 +136,11 @@ export interface PaginationProps extends OcBaseProps { * The Pagination pageCount (use when pageSizes are not necessary). */ pageCount?: number; + /** + * Represents the number of pages visible at any given time apart from + * Previous, Next, Quick Previous and Quick Next buttons. + */ + pagerSize?: PagerSizeOptions; /** * The Pagination pageSize. * @default 10 @@ -199,6 +209,7 @@ export interface PaginationProps extends OcBaseProps { */ totalText?: string; /** + * @deprecated Use pagerSize instead. * Represents the number of list items (pages) are visible at any given time. * @default PaginationVisiblePagerCountSizeOptions.Large */ diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 50a5f312b..e0b4dde59 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -722,6 +722,7 @@ export const Select: FC = React.forwardRef( {/* When Dropdown is hidden, place Pills outside the reference element */} {!dropdownVisible && showPills() ? getPills() : null} = React.forwardRef( onVisibleChange={(isVisible) => setDropdownVisibility(isVisible)} overlay={isLoading ? spinner : } showDropdown={showDropdown} - tabIndex={-1} // Defer focus to the TextInput visible={ dropdownVisible && (showEmptyDropdown || diff --git a/src/components/Select/__snapshots__/Select.test.tsx.snap b/src/components/Select/__snapshots__/Select.test.tsx.snap index e22bf152e..0e5699393 100644 --- a/src/components/Select/__snapshots__/Select.test.tsx.snap +++ b/src/components/Select/__snapshots__/Select.test.tsx.snap @@ -9,13 +9,8 @@ exports[`Select Renders with default value 1`] = ` class="select-dropdown-main-wrapper main-wrapper" >