diff --git a/core/src/components/alert/alert-interface.ts b/core/src/components/alert/alert-interface.ts index 5704b35e45..898ca322e8 100644 --- a/core/src/components/alert/alert-interface.ts +++ b/core/src/components/alert/alert-interface.ts @@ -36,6 +36,7 @@ export interface AlertInput { max?: string | number; cssClass?: string | string[]; attributes?: AlertInputAttributes | AlertTextareaAttributes; + tabindex?: number; } export interface AlertTextareaAttributes extends JSXBase.TextareaHTMLAttributes {} diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index 42fbca8098..89e1468477 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, forceUpdate, h } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Method, Prop, Watch, forceUpdate, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; import { AlertButton, AlertInput, AlertInputAttributes, AlertTextareaAttributes, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface'; @@ -130,6 +130,59 @@ export class Alert implements ComponentInterface, OverlayInterface { */ @Event({ eventName: 'ionAlertDidDismiss' }) didDismiss!: EventEmitter; + @Listen('keydown', { target: 'document' }) + onKeydown(ev: any) { + const inputTypes = new Set(this.processedInputs.map(i => i.type)); + + // The only inputs we want to navigate between using arrow keys are the radios + // ignore the keydown event if it is not on a radio button + if ( + !inputTypes.has('radio') + || (ev.target && !this.el.contains(ev.target)) + || ev.target.classList.contains('alert-button')) + { + return; + } + + // Get all radios inside of the radio group and then + // filter out disabled radios since we need to skip those + const query = this.el.querySelectorAll('.alert-radio') as NodeListOf; + const radios = Array.from(query).filter(radio => !radio.disabled); + + // The focused radio is the one that shares the same id as + // the event target + const index = radios.findIndex(radio => radio.id === ev.target.id); + + // We need to know what the next radio element should + // be in order to change the focus + let nextEl: HTMLButtonElement | undefined; + + // If hitting arrow down or arrow right, move to the next radio + // If we're on the last radio, move to the first radio + if (['ArrowDown', 'ArrowRight'].includes(ev.key)) { + nextEl = (index === radios.length - 1) + ? radios[0] + : radios[index + 1]; + } + + // If hitting arrow up or arrow left, move to the previous radio + // If we're on the first radio, move to the last radio + if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) { + nextEl = (index === 0) + ? radios[radios.length - 1] + : radios[index - 1] + } + + if (nextEl && radios.includes(nextEl)) { + const nextProcessed = this.processedInputs.find(input => input.id === nextEl?.id); + + if (nextProcessed) { + this.rbClick(nextProcessed); + nextEl.focus(); + } + } + } + @Watch('buttons') buttonsChanged() { const buttons = this.buttons; @@ -144,12 +197,21 @@ export class Alert implements ComponentInterface, OverlayInterface { inputsChanged() { const inputs = this.inputs; + // Get the first input that is not disabled and the checked one + // If an enabled checked input exists, set it to be the focusable input + // otherwise we default to focus the first input + // This will only be used when the input is type radio + const first = inputs.find(input => !input.disabled); + const checked = inputs.find(input => input.checked && !input.disabled); + const focusable = checked || first; + // An alert can be created with several different inputs. Radios, // checkboxes and inputs are all accepted, but they cannot be mixed. const inputTypes = new Set(inputs.map(i => i.type)); if (inputTypes.has('checkbox') && inputTypes.has('radio')) { console.warn(`Alert cannot mix input types: ${(Array.from(inputTypes.values()).join('/'))}. Please see alert docs for more info.`); } + this.inputType = inputTypes.values().next().value; this.processedInputs = inputs.map((i, index) => ({ type: i.type || 'text', @@ -165,6 +227,7 @@ export class Alert implements ComponentInterface, OverlayInterface { max: i.max, cssClass: i.cssClass || '', attributes: i.attributes || {}, + tabindex: (i.type === 'radio' && i !== focusable) ? -1 : 0 }) as AlertInput); } @@ -241,6 +304,7 @@ export class Alert implements ComponentInterface, OverlayInterface { private rbClick(selectedInput: AlertInput) { for (const input of this.processedInputs) { input.checked = input === selectedInput; + input.tabindex = input === selectedInput ? 0 : -1; } this.activeId = selectedInput.id; safeCall(selectedInput.handler, selectedInput); @@ -333,7 +397,7 @@ export class Alert implements ComponentInterface, OverlayInterface { aria-checked={`${i.checked}`} id={i.id} disabled={i.disabled} - tabIndex={0} + tabIndex={i.tabindex} role="checkbox" class={{ ...getClassMap(i.cssClass), @@ -373,7 +437,7 @@ export class Alert implements ComponentInterface, OverlayInterface { aria-checked={`${i.checked}`} disabled={i.disabled} id={i.id} - tabIndex={0} + tabIndex={i.tabindex} class={{ ...getClassMap(i.cssClass), 'alert-radio-button': true, @@ -411,7 +475,7 @@ export class Alert implements ComponentInterface, OverlayInterface { placeholder={i.placeholder} value={i.value} id={i.id} - tabIndex={0} + tabIndex={i.tabindex} {...i.attributes as AlertTextareaAttributes} disabled={i.attributes?.disabled ?? i.disabled} class={inputClass(i)} @@ -432,7 +496,7 @@ export class Alert implements ComponentInterface, OverlayInterface { max={i.max} value={i.value} id={i.id} - tabIndex={0} + tabIndex={i.tabindex} {...i.attributes as AlertInputAttributes} disabled={i.attributes?.disabled ?? i.disabled} class={inputClass(i)} diff --git a/core/src/components/alert/test/basic/index.html b/core/src/components/alert/test/basic/index.html index a7d4847b91..df7afb9a31 100644 --- a/core/src/components/alert/test/basic/index.html +++ b/core/src/components/alert/test/basic/index.html @@ -206,7 +206,6 @@ type: 'radio', label: 'Radio 1', value: 'value1', - checked: true }, { type: 'radio', @@ -216,7 +215,8 @@ { type: 'radio', label: 'Radio 3', - value: 'value3' + value: 'value3', + checked: true }, { type: 'radio', @@ -241,12 +241,12 @@ role: 'cancel', cssClass: 'secondary', handler: () => { - console.log('Confirm Cancel') + console.log('Confirm Cancel'); } }, { text: 'Ok', - handler: () => { - console.log('Confirm Ok') + handler: (ev) => { + console.log('Confirm Ok', ev); } } ]