6 * Element supporting "sequential focus navigation" using the 'tabindex' attribute.
10 trait TabIndexedElement {
16 protected $tabIndex = null;
21 protected $tabIndexed;
24 * @param array $config Configuration options
25 * @param string|number|null $config['tabIndex'] Tab index value. Use 0 to use default ordering,
26 * use -1 to prevent tab focusing, use null to suppress the `tabindex` attribute. (default: 0)
28 public function initializeTabIndexedElement( array $config = [] ) {
30 $this->tabIndexed = isset( $config['tabIndexed'] ) ? $config['tabIndexed'] : $this;
33 $this->setTabIndex( isset( $config['tabIndex'] ) ? $config['tabIndex'] : 0 );
35 $this->registerConfigCallback( function ( &$config ) {
36 if ( $this->tabIndex !== 0 ) {
37 $config['tabIndex'] = $this->tabIndex;
43 * Set tab index value.
45 * @param string|number|null $tabIndex Tab index value or null for no tab index
48 public function setTabIndex( $tabIndex ) {
49 $tabIndex = preg_match( '/^-?\d+$/', $tabIndex ) ? (int)$tabIndex : null;
51 if ( $this->tabIndex !== $tabIndex ) {
52 $this->tabIndex = $tabIndex;
53 $this->updateTabIndex();
60 * Update the tabIndex attribute, in case of changes to tabIndex or disabled
65 public function updateTabIndex() {
66 $disabled = $this->isDisabled();
67 if ( $this->tabIndex !== null ) {
68 $this->tabIndexed->setAttributes( [
69 // Do not index over disabled elements
70 'tabindex' => $disabled ? -1 : $this->tabIndex,
71 // ChromeVox and NVDA do not seem to inherit this from parent elements
72 'aria-disabled' => ( $disabled ? 'true' : 'false' )
75 $this->tabIndexed->removeAttributes( [ 'tabindex', 'aria-disabled' ] );
81 * Get tab index value.
83 * @return number|null Tab index value
85 public function getTabIndex() {
86 return $this->tabIndex;
90 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
92 * If the element already has an ID then that is returned, otherwise unique ID is
93 * generated, set on the element, and returned.
95 * @return string|null The ID of the focusable element
97 public function getInputId() {
98 $id = $this->tabIndexed->getAttribute( 'id' );
100 if ( !$this->isLabelableNode( $this->tabIndexed ) ) {
104 if ( $id === null ) {
105 $id = Tag::generateElementId();
106 $this->tabIndexed->setAttributes( [ 'id' => $id ] );
113 * Whether the node is 'labelable' according to the HTML spec
114 * (i.e., whether it can be interacted with through a `<label for="…">`).
115 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
120 private function isLabelableNode( Tag $tag ) {
121 $labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ];
122 $tagName = strtolower( $tag->getTag() );
124 if ( $tagName === 'input' && $tag->getAttribute( 'type' ) !== 'hidden' ) {
127 if ( in_array( $tagName, $labelableTags, true ) ) {