Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/eui/changelogs/upcoming/9201.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- Added beta prop `hasAriaDisabled` to all base button components: `EuiButton`, `EuiButtonEmpty`, `EuiButtonIcon`, `EuibuttonGroup`, `EuiFilterButton`
- Added `euiDisabledSelector` variable that combines CSS selectors `:disabled` and `[aria-disabled="true"]`
- Added custom test matchers that check for both `disabled` and `aria-disabled` attributes:
- React testing Library: `.toBeEuiDisabled()`
- Enzyme: `.toHaveEuiDisabledProp()`
- Cypress: `should('be.euiDisabled)`

1 change: 1 addition & 0 deletions packages/eui/cypress/support/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import './keyboard/repeatRealPress';
import './copy/select_and_copy';
import './setup/mount';
import './setup/realMount';
import './setup/matchers';
import './css/cssVar';
import './helpers/wait_for_position_to_settle';

Expand Down
10 changes: 10 additions & 0 deletions packages/eui/cypress/support/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,15 @@ declare global {
*/
waitForPositionToSettle(): Chainable<JQuery<HTMLElement>>;
}
interface Chainer<Subject> {
(chainer: 'be.euiDisabled'): Chainable<Subject>;
(chainer: 'be.euiEnabled'): Chainable<Subject>;
}
}
namespace Chai {
interface Assertion {
euiDisabled: Assertion;
euiEnabled: Assertion;
}
}
}
11 changes: 11 additions & 0 deletions packages/eui/cypress/support/setup/matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { setupEuiCypressMatchers } from '../../../src/test/cypress';

setupEuiCypressMatchers();
6 changes: 4 additions & 2 deletions packages/eui/cypress/support/setup/realMount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@

import React, { ReactNode } from 'react';
import './mount';
import { MountOptions } from './mount';

const realMountCommand = (children: ReactNode) => {
const realMountCommand = (children: ReactNode, options: MountOptions = {}) => {
cy.mount(
<>
<div
data-test-subj="cypress-real-event-target"
style={{ height: '1px', width: '1px' }}
/>
{children}
</>
</>,
options
).then(() => {
cy.get('[data-test-subj="cypress-real-event-target"]').realClick({
position: 'topLeft',
Expand Down
16 changes: 11 additions & 5 deletions packages/eui/scripts/compile-eui.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ async function compileBundle() {
'optimize/es/test',
].map((dir) => path.join(packageRootDir, dir));

const testRtlDTSFiles = new glob.Glob('test/rtl/**/*.d.ts', {
const testDirectories = ['rtl', 'enzyme'];
const testDTSFiles = new glob.Glob('test/**/*.d.ts', {
cwd: srcDir,
realpath: true,
});
Expand Down Expand Up @@ -278,12 +279,17 @@ async function compileBundle() {
},
});

await fs.mkdir(path.join(dir, 'rtl'), { recursive: true });
for (const testDir of testDirectories) {
await fs.mkdir(path.join(dir, testDir), { recursive: true });
}

for await (const filePath of testRtlDTSFiles) {
for await (const filePath of testDTSFiles) {
const fullPath = path.join(srcDir, filePath);
const baseName = path.basename(filePath);
await fs.copyFile(fullPath, path.join(dir, 'rtl', baseName));

const relativePath = filePath.replace(/^test\//, '');
const destPath = path.join(dir, relativePath);

await fs.copyFile(fullPath, destPath);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/eui/scripts/jest/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const config = {
setupFilesAfterEnv: [
'<rootDir>/scripts/jest/setup/polyfills.js',
'<rootDir>/scripts/jest/setup/unmount_enzyme.js',
'<rootDir>/scripts/jest/setup/matchers.js',
],
coverageDirectory: '<rootDir>/reports/jest-coverage',
coverageReporters: ['json', 'html'],
Expand Down
7 changes: 7 additions & 0 deletions packages/eui/scripts/jest/setup/matchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const setupEuiMatchers =
require('../../../src/test/rtl/matchers.ts').setupEuiMatchers;
const setupEuiEnzymeMatchers =
require('../../../src/test/enzyme/enzyme_matchers.ts').setupEuiEnzymeMatchers;

setupEuiMatchers();
setupEuiEnzymeMatchers();
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ exports[`EuiButton props iconType is rendered 1`] = `
exports[`EuiButton props isDisabled is rendered 1`] = `
<button
class="euiButton emotion-euiButtonDisplay-m-defaultMinWidth-isDisabled-base-disabled"
data-test-subj="button"
disabled=""
type="button"
>
Expand Down
1 change: 1 addition & 0 deletions packages/eui/src/components/button/button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const meta: Meta<EuiButtonProps> = {
iconSide: 'left',
fullWidth: false,
isDisabled: false,
hasAriaDisabled: false,
isLoading: false,
isSelected: false,
},
Expand Down
23 changes: 22 additions & 1 deletion packages/eui/src/components/button/button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ describe('EuiButton', () => {

describe('isDisabled', () => {
it('is rendered', () => {
const { container } = render(<EuiButton isDisabled />);
const { container, getByTestSubject } = render(
<EuiButton isDisabled data-test-subj="button" />
);

const button = getByTestSubject('button');

expect(container.firstChild).toMatchSnapshot();
expect(button).toBeEuiDisabled();
expect(button).toHaveAttribute('disabled', '');
expect(button).not.toHaveAttribute('aria-disabled');
});

it('renders a button even when href is defined', () => {
Expand All @@ -61,6 +68,20 @@ describe('EuiButton', () => {
});
});

describe('hasAriaDisabled', () => {
it('renders `aria-disabled` when `isDisabled=true`', () => {
const { getByTestSubject } = render(
<EuiButton hasAriaDisabled isDisabled data-test-subj="button" />
);

const button = getByTestSubject('button');

expect(button).toBeEuiDisabled();
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).not.toHaveAttribute('disabled');
});
});

describe('isLoading', () => {
it('is rendered', () => {
const { container } = render(<EuiButton isLoading />);
Expand Down
7 changes: 2 additions & 5 deletions packages/eui/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PropsForButton,
} from '../common';

import { EuiDisabledProps } from '../../services/hooks/useEuiDisabledElement';
import {
BUTTON_COLORS,
useEuiButtonColorCSS,
Expand All @@ -34,7 +35,7 @@ export type EuiButtonColor = _EuiExtendedButtonColor;
export const SIZES = ['s', 'm'] as const;
export type EuiButtonSize = (typeof SIZES)[number];

interface BaseProps {
interface BaseProps extends EuiDisabledProps {
children?: ReactNode;
/**
* Make button a solid color for prominence
Expand All @@ -55,10 +56,6 @@ interface BaseProps {
* Use size `s` in confined spaces
*/
size?: EuiButtonSize;
/**
* `disabled` is also allowed
*/
isDisabled?: boolean;
}

export interface EuiButtonProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ export const euiButtonDisplayStyles = (euiThemeContext: UseEuiTheme) => {
// States
isDisabled: css`
cursor: not-allowed;

/* prevent user (mouse) interactions for custom disabled buttons.
Covers user interaction only. Programmatic event handling is done in the \`useEuiDisabledElement\` hook */
&[aria-disabled='true'] {
pointer-events: none;

> * {
pointer-events: none;
}
}
`,
fullWidth: css`
display: block;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@ import React, {

// @ts-ignore module doesn't export `createElement`
import { createElement } from '@emotion/react';
import { getSecureRelForTarget, useEuiMemoizedStyles } from '../../../services';

import {
getSecureRelForTarget,
useCombinedRefs,
useEuiMemoizedStyles,
} from '../../../services';
import { validateHref } from '../../../services/security/href_validator';
import {
EuiDisabledProps,
useEuiDisabledElement,
} from '../../../services/hooks/useEuiDisabledElement';
import {
CommonProps,
ExclusiveUnion,
PropsForAnchor,
PropsForButton,
} from '../../common';

import { euiButtonDisplayStyles } from './_button_display.styles';
import {
EuiButtonDisplayContent,
EuiButtonDisplayContentProps,
EuiButtonDisplayContentType,
} from './_button_display_content';
import { validateHref } from '../../../services/security/href_validator';

const SIZES = ['xs', 's', 'm'] as const;
export type EuiButtonDisplaySizes = (typeof SIZES)[number];
Expand All @@ -41,7 +47,8 @@ export type EuiButtonDisplaySizes = (typeof SIZES)[number];
* `iconType`, `iconSide`, and `textProps`
*/
export interface EuiButtonDisplayCommonProps
extends EuiButtonDisplayContentProps,
extends Omit<EuiButtonDisplayContentProps, 'disabled'>,
EuiDisabledProps,
CommonProps {
element?: 'a' | 'button' | 'span';
children?: ReactNode;
Expand Down Expand Up @@ -119,6 +126,7 @@ export const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
size = 'm',
isDisabled,
disabled,
hasAriaDisabled = false,
isLoading,
isSelected,
fullWidth,
Expand All @@ -139,6 +147,15 @@ export const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
isLoading,
});

const { ref: disabledRef, ...disabledButtonProps } =
useEuiDisabledElement<HTMLButtonElement>({
isDisabled: buttonIsDisabled,
hasAriaDisabled,
onKeyDown: rest.onKeyDown,
});

const setCombinedRef = useCombinedRefs([disabledRef, ref]);

const styles = useEuiMemoizedStyles(euiButtonDisplayStyles);
const cssStyles = [
styles.euiButtonDisplay,
Expand Down Expand Up @@ -166,13 +183,15 @@ export const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
);

const element = buttonIsDisabled ? 'button' : href ? 'a' : _element;
let elementProps = {};
// Element-specific attributes
const elementProps = {
ref: setCombinedRef,
};
let buttonProps = {};

if (element === 'button') {
elementProps = {
...elementProps,
disabled: buttonIsDisabled,
buttonProps = {
'aria-pressed': isSelected,
...disabledButtonProps,
};
}

Expand All @@ -196,10 +215,10 @@ export const EuiButtonDisplay = forwardRef<HTMLElement, EuiButtonDisplayProps>(
{
css: cssStyles,
style: minWidth ? { ...style, minInlineSize: minWidth } : style,
ref,
...elementProps,
...relObj,
...rest,
...buttonProps,
},
innerNode
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ exports[`EuiButtonEmpty props iconType is rendered 1`] = `
exports[`EuiButtonEmpty props isDisabled is rendered 1`] = `
<button
class="euiButtonEmpty emotion-euiButtonDisplay-euiButtonEmpty-m-empty-disabled-isDisabled"
data-test-subj="button"
disabled=""
type="button"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const meta: Meta<EuiButtonEmptyProps> = {
iconSize: 'm',
iconSide: 'left',
isDisabled: false,
hasAriaDisabled: false,
isLoading: false,
isSelected: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@ describe('EuiButtonEmpty', () => {
describe('props', () => {
describe('isDisabled', () => {
it('is rendered', () => {
const { container } = render(<EuiButtonEmpty isDisabled />);
const { container, getByTestSubject } = render(
<EuiButtonEmpty isDisabled data-test-subj="button" />
);

const button = getByTestSubject('button');

expect(container.firstChild).toMatchSnapshot();
expect(button).toBeEuiDisabled();
expect(button).toHaveAttribute('disabled', '');
expect(button).not.toHaveAttribute('aria-disabled');
});

it('renders a button even when href is defined', () => {
Expand All @@ -50,6 +57,20 @@ describe('EuiButtonEmpty', () => {
});
});

describe('hasAriaDisabled', () => {
it('renders `aria-disabled` when `isDisabled=true`', () => {
const { getByTestSubject } = render(
<EuiButtonEmpty hasAriaDisabled isDisabled data-test-subj="button" />
);

const button = getByTestSubject('button');

expect(button).toBeEuiDisabled();
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).not.toHaveAttribute('disabled');
});
});

describe('isLoading', () => {
it('is rendered', () => {
const { container } = render(<EuiButtonEmpty isLoading />);
Expand Down
Loading