Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 0947a02

Browse files
committed
feat(snackbar): Implement accessibility focus logic
1 parent c72990a commit 0947a02

File tree

2 files changed

+83
-26
lines changed

2 files changed

+83
-26
lines changed

packages/mdc-snackbar/foundation.js

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
3636
setActionText: (/* actionText: string */) => {},
3737
setActionAriaHidden: () => {},
3838
unsetActionAriaHidden: () => {},
39+
registerFocusHandler: () => {},
40+
deregisterFocusHandler: () => {},
41+
registerBlurHandler: () => {},
42+
deregisterBlurHandler: () => {},
3943
registerActionClickHandler: (/* handler: EventListener */) => {},
4044
deregisterActionClickHandler: (/* handler: EventListener */) => {},
4145
registerTransitionEndHandler: (/* handler: EventListener */) => {},
@@ -51,9 +55,37 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
5155
super(Object.assign(MDCSnackbarFoundation.defaultAdapter, adapter));
5256

5357
this.active_ = false;
58+
this.actionWasClicked_ = false;
5459
this.dismissOnAction_ = true;
60+
this.firstFocus_ = true;
61+
this.snackbarHasFocus_ = false;
62+
this.snackbarData_ = null;
5563
this.queue_ = [];
56-
this.actionClickHandler_ = () => this.invokeAction_();
64+
65+
this.actionClickHandler_ = () => {
66+
this.actionWasClicked_ = true;
67+
this.invokeAction_();
68+
};
69+
this.focusHandler_ = () => {
70+
if (this.firstFocus_) {
71+
this.setFocusOnAction_();
72+
}
73+
74+
this.firstFocus_ = false;
75+
};
76+
this.blurHandler_ = () => {
77+
this.snackbarHasFocus_ = false;
78+
clearTimeout(this.timeoutId_);
79+
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
80+
};
81+
this.visibilitychangeHandler_ = () => {
82+
clearTimeout(this.timeoutId_);
83+
this.snackbarHasFocus_ = true;
84+
85+
if (!this.adapter_.visibilityIsHidden()) {
86+
setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
87+
}
88+
};
5789
}
5890

5991
init() {
@@ -64,6 +96,8 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
6496

6597
destroy() {
6698
this.adapter_.deregisterActionClickHandler(this.actionClickHandler_);
99+
this.adapter_.deregisterFocusHandler(this.focusHandler_);
100+
this.adapter_.deregisterBlurHandler(this.focusHandler_);
67101
}
68102

69103
dismissesOnAction() {
@@ -75,37 +109,43 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
75109
}
76110

77111
show(data) {
78-
if (!data) {
112+
this.snackbarData_ = data;
113+
this.firstFocus_ = true;
114+
this.adapter_.registerVisbilityChangeHandler(this.visibilitychangeHandler_);
115+
this.adapter_.registerFocusHandler(this.focusHandler_);
116+
this.adapter_.registerBlurHandler(this.blurHandler_);
117+
118+
if (!this.snackbarData_) {
79119
throw new Error(
80120
'Please provide a data object with at least a message to display.');
81121
}
82-
if (!data.message) {
122+
if (!this.snackbarData_.message) {
83123
throw new Error('Please provide a message to be displayed.');
84124
}
85-
if (data.actionHandler && !data.actionText) {
125+
if (this.snackbarData_.actionHandler && !this.snackbarData_.actionText) {
86126
throw new Error('Please provide action text with the handler.');
87127
}
88128

89129
if (this.active) {
90-
this.queue_.push(data);
130+
this.queue_.push(this.snackbarData_);
91131
return;
92132
}
93133

94134
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
95135
const {MESSAGE_TIMEOUT} = numbers;
96136

97-
this.adapter_.setMessageText(data.message);
137+
this.adapter_.setMessageText(this.snackbarData_.message);
98138

99-
if (data.multiline) {
139+
if (this.snackbarData_.multiline) {
100140
this.adapter_.addClass(MULTILINE);
101-
if (data.actionOnBottom) {
141+
if (this.snackbarData_.actionOnBottom) {
102142
this.adapter_.addClass(ACTION_ON_BOTTOM);
103143
}
104144
}
105145

106-
if (data.actionHandler) {
107-
this.adapter_.setActionText(data.actionText);
108-
this.actionHandler_ = data.actionHandler;
146+
if (this.snackbarData_.actionHandler) {
147+
this.adapter_.setActionText(this.snackbarData_.actionText);
148+
this.actionHandler_ = this.snackbarData_.actionHandler;
109149
this.setActionHidden_(false);
110150
} else {
111151
this.setActionHidden_(true);
@@ -117,7 +157,12 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
117157
this.adapter_.addClass(ACTIVE);
118158
this.adapter_.unsetAriaHidden();
119159

120-
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), data.timeout || MESSAGE_TIMEOUT);
160+
this.timeoutId_ = setTimeout(this.cleanup_.bind(this), this.snackbarData_.timeout || MESSAGE_TIMEOUT);
161+
}
162+
163+
setFocusOnAction_() {
164+
this.adapter_.setFocus();
165+
this.snackbarHasFocus_ = true;
121166
}
122167

123168
invokeAction_() {
@@ -136,21 +181,25 @@ export default class MDCSnackbarFoundation extends MDCFoundation {
136181
}
137182

138183
cleanup_() {
139-
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
140-
141-
this.adapter_.removeClass(ACTIVE);
142-
143-
const handler = () => {
144-
this.adapter_.deregisterTransitionEndHandler(handler);
145-
this.adapter_.removeClass(MULTILINE);
146-
this.adapter_.removeClass(ACTION_ON_BOTTOM);
147-
this.setActionHidden_(true);
148-
this.adapter_.setAriaHidden();
149-
this.active_ = false;
150-
this.showNext_();
151-
};
184+
if (!this.snackbarHasFocus_ || this.actionWasClicked_) {
185+
const {ACTIVE, MULTILINE, ACTION_ON_BOTTOM} = cssClasses;
186+
187+
this.adapter_.removeClass(ACTIVE);
188+
189+
const handler = () => {
190+
this.adapter_.deregisterTransitionEndHandler(handler);
191+
this.adapter_.removeClass(MULTILINE);
192+
this.adapter_.removeClass(ACTION_ON_BOTTOM);
193+
this.setActionHidden_(true);
194+
this.adapter_.setAriaHidden();
195+
this.active_ = false;
196+
this.snackbarHasFocus_ = false;
197+
clearTimeout(this.timeoutId_);
198+
this.showNext_();
199+
};
152200

153-
this.adapter_.registerTransitionEndHandler(handler);
201+
this.adapter_.registerTransitionEndHandler(handler);
202+
}
154203
}
155204

156205
showNext_() {

packages/mdc-snackbar/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ export class MDCSnackbar extends MDCComponent {
4747
unsetActionAriaHidden: () => getActionButton().removeAttribute('aria-hidden'),
4848
setActionText: (text) => { getActionButton().textContent = text; },
4949
setMessageText: (text) => { getText().textContent = text; },
50+
setFocus: () => getActionButton().focus(),
51+
visibilityIsHidden: () => document.hidden,
52+
registerFocusHandler: (handler) => document.body.addEventListener('focus', handler, true),
53+
deregisterFocusHandler: (handler) => document.body.addEventListener('focus', handler, true),
54+
registerBlurHandler: (handler) => getActionButton().addEventListener('blur', handler, true),
55+
deregisterBlurHandler: (handler) => getActionButton().addEventListener('blur', handler, true),
56+
registerVisbilityChangeHandler: (handler) => document.addEventListener('visibilitychange', handler),
57+
deregisterVisbilityChangeHandler: (handler) => document.addEventListener('visibilitychange', handler),
5058
registerActionClickHandler: (handler) => getActionButton().addEventListener('click', handler),
5159
deregisterActionClickHandler: (handler) => getActionButton().removeEventListener('click', handler),
5260
registerTransitionEndHandler:

0 commit comments

Comments
 (0)