diff --git a/.changeset/thick-owls-give.md b/.changeset/thick-owls-give.md new file mode 100644 index 00000000000..244ba29bd55 --- /dev/null +++ b/.changeset/thick-owls-give.md @@ -0,0 +1,8 @@ +--- +'@siemens/ix-angular': patch +'@siemens/ix-react': patch +'@siemens/ix': patch +'@siemens/ix-vue': patch +--- + +Prevent _ix-group_ from crashing during construction diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 535f3df4385..1d30df19a24 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -817,7 +817,7 @@ export declare interface IxDrawer extends Components.IxDrawer { @ProxyCmp({ - inputs: ['anchor', 'closeBehavior', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], + inputs: ['anchor', 'closeBehavior', 'disableFocusHandling', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], methods: ['updatePosition'] }) @Component({ @@ -825,12 +825,13 @@ export declare interface IxDrawer extends Components.IxDrawer { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['anchor', 'closeBehavior', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], - outputs: ['showChanged'], + inputs: ['anchor', 'closeBehavior', 'disableFocusHandling', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], + outputs: ['showChange', 'showChanged'], standalone: false }) export class IxDropdown { protected el: HTMLIxDropdownElement; + @Output() showChange = new EventEmitter>(); @Output() showChanged = new EventEmitter>(); constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); @@ -841,7 +842,11 @@ export class IxDropdown { export declare interface IxDropdown extends Components.IxDropdown { /** - * Fire event after visibility of dropdown has changed + * Fire event before visibility of dropdown has changed, preventing event will cancel showing dropdown + */ + showChange: EventEmitter>; + /** + * Fire event after visibility of dropdown has changed, preventing event will prevent nothing */ showChanged: EventEmitter>; } @@ -2306,7 +2311,7 @@ export declare interface IxRow extends Components.IxRow {} @ProxyCmp({ - inputs: ['allowClear', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], + inputs: ['allowClear', 'ariaLabelAddItem', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], methods: ['getNativeInputElement', 'focusInput'] }) @Component({ @@ -2314,7 +2319,7 @@ export declare interface IxRow extends Components.IxRow {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['allowClear', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], + inputs: ['allowClear', 'ariaLabelAddItem', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], outputs: ['valueChange', 'inputChange', 'addItem', 'ixBlur'], standalone: false }) diff --git a/packages/angular/standalone/src/components.ts b/packages/angular/standalone/src/components.ts index 30696df496b..7734add0c8d 100644 --- a/packages/angular/standalone/src/components.ts +++ b/packages/angular/standalone/src/components.ts @@ -918,7 +918,7 @@ export declare interface IxDrawer extends Components.IxDrawer { @ProxyCmp({ defineCustomElementFn: defineIxDropdown, - inputs: ['anchor', 'closeBehavior', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], + inputs: ['anchor', 'closeBehavior', 'disableFocusHandling', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], methods: ['updatePosition'] }) @Component({ @@ -926,11 +926,12 @@ export declare interface IxDrawer extends Components.IxDrawer { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['anchor', 'closeBehavior', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], - outputs: ['showChanged'], + inputs: ['anchor', 'closeBehavior', 'disableFocusHandling', 'header', 'placement', 'positioningStrategy', 'show', 'suppressAutomaticPlacement', 'trigger'], + outputs: ['showChange', 'showChanged'], }) export class IxDropdown { protected el: HTMLIxDropdownElement; + @Output() showChange = new EventEmitter>(); @Output() showChanged = new EventEmitter>(); constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); @@ -941,7 +942,11 @@ export class IxDropdown { export declare interface IxDropdown extends Components.IxDropdown { /** - * Fire event after visibility of dropdown has changed + * Fire event before visibility of dropdown has changed, preventing event will cancel showing dropdown + */ + showChange: EventEmitter>; + /** + * Fire event after visibility of dropdown has changed, preventing event will prevent nothing */ showChanged: EventEmitter>; } @@ -2407,7 +2412,7 @@ export declare interface IxRow extends Components.IxRow {} @ProxyCmp({ defineCustomElementFn: defineIxSelect, - inputs: ['allowClear', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], + inputs: ['allowClear', 'ariaLabelAddItem', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], methods: ['getNativeInputElement', 'focusInput'] }) @Component({ @@ -2415,7 +2420,7 @@ export declare interface IxRow extends Components.IxRow {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['allowClear', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], + inputs: ['allowClear', 'ariaLabelAddItem', 'ariaLabelChevronDownIconButton', 'ariaLabelClearIconButton', 'collapseMultipleSelection', 'disabled', 'dropdownMaxWidth', 'dropdownWidth', 'editable', 'helperText', 'hideListHeader', 'i18nAllSelected', 'i18nNoMatches', 'i18nPlaceholder', 'i18nPlaceholderEditable', 'i18nSelectListHeader', 'infoText', 'invalidText', 'label', 'mode', 'name', 'readonly', 'required', 'showTextAsTooltip', 'validText', 'value', 'warningText'], outputs: ['valueChange', 'inputChange', 'addItem', 'ixBlur'], }) export class IxSelect { diff --git a/packages/core/playwright.config.ts b/packages/core/playwright.config.ts index be64aa23db6..d02d439ce2a 100644 --- a/packages/core/playwright.config.ts +++ b/packages/core/playwright.config.ts @@ -61,7 +61,7 @@ const config: PlaywrightTestConfig = { command: 'pnpm run host-root', port: 8080, }, - retries: process.env.CI ? 3 : 1, + retries: 3, }; export default config; diff --git a/packages/core/scss/mixins/shadow-dom/_focus-visible.scss b/packages/core/scss/mixins/shadow-dom/_focus-visible.scss new file mode 100644 index 00000000000..607263e418d --- /dev/null +++ b/packages/core/scss/mixins/shadow-dom/_focus-visible.scss @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +@mixin ix-focus-visible() { + outline: 1px solid var(--theme-color-focus-bdr) !important; + outline-offset: var(--theme-btn--focus--outline-offset) !important; +} diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 954fde3cfb6..835b9ef8e3e 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1295,6 +1295,11 @@ export namespace Components { * @default 'both' */ "closeBehavior": CloseBehavior; + /** + * Suppress automatic focus when the dropdown is shown + * @default false + */ + "disableFocusHandling": boolean; /** * @default false */ @@ -1415,6 +1420,10 @@ export namespace Components { "disabled": boolean; "emitItemClick": () => Promise; "getDropdownItemElement": () => Promise; + /** + * @default false + */ + "hasVisualFocus": boolean; /** * Display hover state * @default false @@ -1436,6 +1445,11 @@ export namespace Components { * @default false */ "suppressChecked": boolean; + /** + * Can has focus + * @default false + */ + "suppressFocus": boolean; } interface IxDropdownQuickActions { } @@ -2983,12 +2997,18 @@ export namespace Components { * @default false */ "allowClear": boolean; + /** + * ARIA label for the add item + * @since TODO: Define + * @default 'Add item' + */ + "ariaLabelAddItem": string; /** * ARIA label for the chevron down icon button Will be set as aria-label on the nested HTML button element * @since 3.2.0 * @default 'Open select dropdown' */ - "ariaLabelChevronDownIconButton"?: string; + "ariaLabelChevronDownIconButton": string; /** * ARIA label for the clear icon button Will be set as aria-label on the nested HTML button element * @since 3.2.0 @@ -3116,6 +3136,10 @@ export namespace Components { } interface IxSelectItem { "getDropdownItemElement": () => Promise; + /** + * @default false + */ + "hasVisualFocus": boolean; /** * @default false */ @@ -4720,7 +4744,11 @@ declare global { new (): HTMLIxDrawerElement; }; interface HTMLIxDropdownElementEventMap { + "showChange": boolean; "showChanged": boolean; + "experimentalRequestFocus": { + keyEvent: KeyboardEvent; + }; } interface HTMLIxDropdownElement extends Components.IxDropdown, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLIxDropdownElement, ev: IxDropdownCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -7147,6 +7175,11 @@ declare namespace LocalJSX { * @default 'both' */ "closeBehavior"?: CloseBehavior; + /** + * Suppress automatic focus when the dropdown is shown + * @default false + */ + "disableFocusHandling"?: boolean; /** * @default false */ @@ -7168,7 +7201,17 @@ declare namespace LocalJSX { alignmentAxis?: number; }; /** - * Fire event after visibility of dropdown has changed + * Will be fired only after dropdown changed visibility to "true" + */ + "onExperimentalRequestFocus"?: (event: IxDropdownCustomEvent<{ + keyEvent: KeyboardEvent; + }>) => void; + /** + * Fire event before visibility of dropdown has changed, preventing event will cancel showing dropdown + */ + "onShowChange"?: (event: IxDropdownCustomEvent) => void; + /** + * Fire event after visibility of dropdown has changed, preventing event will prevent nothing */ "onShowChanged"?: (event: IxDropdownCustomEvent) => void; "overwriteDropdownStyle"?: (delegate: { @@ -7264,6 +7307,10 @@ declare namespace LocalJSX { * @default false */ "disabled"?: boolean; + /** + * @default false + */ + "hasVisualFocus"?: boolean; /** * Display hover state * @default false @@ -7286,6 +7333,11 @@ declare namespace LocalJSX { * @default false */ "suppressChecked"?: boolean; + /** + * Can has focus + * @default false + */ + "suppressFocus"?: boolean; } interface IxDropdownQuickActions { } @@ -8951,6 +9003,12 @@ declare namespace LocalJSX { * @default false */ "allowClear"?: boolean; + /** + * ARIA label for the add item + * @since TODO: Define + * @default 'Add item' + */ + "ariaLabelAddItem"?: string; /** * ARIA label for the chevron down icon button Will be set as aria-label on the nested HTML button element * @since 3.2.0 @@ -9085,6 +9143,10 @@ declare namespace LocalJSX { "warningText"?: string; } interface IxSelectItem { + /** + * @default false + */ + "hasVisualFocus"?: boolean; /** * @default false */ diff --git a/packages/core/src/components/avatar/avatar.scss b/packages/core/src/components/avatar/avatar.scss index af17f427ba0..1fd2818fd69 100644 --- a/packages/core/src/components/avatar/avatar.scss +++ b/packages/core/src/components/avatar/avatar.scss @@ -12,6 +12,7 @@ @use 'mixins/text-truncation'; @use 'mixins/hover'; @use 'mixins/scrollbar'; +@use 'mixins/shadow-dom/focus-visible' as focus-visible; :host { display: flex; @@ -119,3 +120,9 @@ transform: scale(0.8); } } + +:host(.avatar-button.ix-focused) { + button { + @include focus-visible.ix-focus-visible(); + } +} diff --git a/packages/core/src/components/avatar/avatar.tsx b/packages/core/src/components/avatar/avatar.tsx index e9a5e6abece..6b261772457 100644 --- a/packages/core/src/components/avatar/avatar.tsx +++ b/packages/core/src/components/avatar/avatar.tsx @@ -233,7 +233,10 @@ export class Avatar { if (this.isClosestApplicationHeader) { return ( - + = ( const commonAttributes = { ...ariaAttributes, - tabindex: props.disabled ? -1 : (props.tabIndex ?? 0), + // tabindex: props.disabled ? -1 : (props.tabIndex ?? 0), class: { ...getButtonClasses( props.variant, diff --git a/packages/core/src/components/button/button-mixin.scss b/packages/core/src/components/button/button-mixin.scss index b31fc213088..7e7879075ac 100644 --- a/packages/core/src/components/button/button-mixin.scss +++ b/packages/core/src/components/button/button-mixin.scss @@ -19,9 +19,7 @@ border-top-right-radius: var(--ix-button-border-radius-right); border-bottom-right-radius: var(--ix-button-border-radius-right); - &, - &.focus, - &:focus-visible { + & { background-color: var(--theme-btn-#{$name}--background); color: var(--theme-btn-#{$name}--color); @@ -32,10 +30,10 @@ border-style: solid; } - @include hover.focus-visible { - outline: 1px solid var(--theme-color-focus-bdr); - outline-offset: var(--theme-btn--focus--outline-offset); - } + // @include hover.focus-visible { + // outline: 1px solid var(--theme-color-focus-bdr); + // outline-offset: var(--theme-btn--focus--outline-offset); + // } &.selected { background-color: var(--theme-btn-#{$name}--background--pressed); @@ -160,6 +158,21 @@ :host(.disabled) { cursor: default; } + + :host(.ix-focused) { + .btn { + outline: 1px solid var(--theme-color-focus-bdr) !important; + outline-offset: var(--theme-btn--focus--outline-offset) !important; + } + } + + :host(:focus-visible) { + outline: none; + + .btn { + outline: none; + } + } } @mixin all-btn-variants { diff --git a/packages/core/src/components/button/button.tsx b/packages/core/src/components/button/button.tsx index 796a635433a..b5a1aa94451 100644 --- a/packages/core/src/components/button/button.tsx +++ b/packages/core/src/components/button/button.tsx @@ -190,7 +190,6 @@ export class Button implements IxButtonComponent { onClick: () => this.dispatchFormEvents(), type: this.type, alignment: this.alignment, - tabIndex: 0, ariaAttributes: { 'aria-label': this.ariaLabelButton, }, @@ -203,6 +202,7 @@ export class Button implements IxButtonComponent { diff --git a/packages/core/src/components/dropdown-item/dropdown-item.scss b/packages/core/src/components/dropdown-item/dropdown-item.scss index bac1dca71a9..f5de53415e5 100644 --- a/packages/core/src/components/dropdown-item/dropdown-item.scss +++ b/packages/core/src/components/dropdown-item/dropdown-item.scss @@ -52,10 +52,6 @@ $focusBorder: 0.0625rem; @include applyItemWidth(1rem, $itemPaddingRight); } - .dropdown-item:focus-visible { - border-color: var(--theme-color-focus-bdr); - } - .dropdown-item-checked { display: flex; align-items: center; @@ -117,3 +113,20 @@ $focusBorder: 0.0625rem; color: var(--theme-color-weak-text) !important; } } + +:host(.ix-focused) { + outline: none; + + .dropdown-item { + border-color: var(--theme-color-focus-bdr); + } +} + +:host(:focus-visible) { + outline: none !important; +} + +:host(.outline-visible) { + outline: 0.0625rem solid var(--theme-color-focus-bdr); + outline-offset: -0.0625rem; +} diff --git a/packages/core/src/components/dropdown-item/dropdown-item.tsx b/packages/core/src/components/dropdown-item/dropdown-item.tsx index b09c136018f..2323613ca2b 100644 --- a/packages/core/src/components/dropdown-item/dropdown-item.tsx +++ b/packages/core/src/components/dropdown-item/dropdown-item.tsx @@ -69,12 +69,22 @@ export class DropdownItem implements DropdownItemWrapper { */ @Prop() checked = false; + /** + * Can has focus + * + * @internal + */ + @Prop() suppressFocus = false; + /** @internal */ @Prop() isSubMenu = false; /** @internal */ @Prop() suppressChecked = false; + /** @internal */ + @Prop({ reflect: true }) hasVisualFocus = false; + /** @internal */ @Event() itemClick!: EventEmitter; @@ -106,12 +116,15 @@ export class DropdownItem implements DropdownItemWrapper { 'icon-only': this.isIconOnly(), disabled: this.disabled, submenu: this.isSubMenu, + 'ix-focusable': !this.suppressFocus, + 'outline-visible': this.hasVisualFocus, }} role="listitem" + tabIndex={-1} >