diff --git a/src/accordionpane/tests/functional/AccordionPane.ts b/src/accordionpane/tests/functional/AccordionPane.ts index 2a0372a804..a1b35e1406 100644 --- a/src/accordionpane/tests/functional/AccordionPane.ts +++ b/src/accordionpane/tests/functional/AccordionPane.ts @@ -20,7 +20,7 @@ registerSuite('AccordionPane', { .then((size: { height: number }) => { assert.isBelow(size.height, 50); }) - .findByCssSelector('[role="heading"]') + .findByCssSelector('button') .click() .end() .sleep(DELAY) diff --git a/src/button/Button.ts b/src/button/Button.ts index f53ab40877..443677477a 100644 --- a/src/button/Button.ts +++ b/src/button/Button.ts @@ -70,6 +70,30 @@ export default class Button extends ButtonBase { private _onTouchEnd (event: TouchEvent) { this.properties.onTouchEnd && this.properties.onTouchEnd(event); } private _onTouchCancel (event: TouchEvent) { this.properties.onTouchCancel && this.properties.onTouchCancel(event); } + protected getContent(): DNode[] { + return this.children; + } + + protected getModifierClasses(): (string | null)[] { + const { + disabled, + popup = false, + pressed + } = this.properties; + + return [ + disabled ? css.disabled : null, + popup ? css.popup : null, + pressed ? css.pressed : null + ]; + } + + protected renderPopupIcon(): DNode { + return v('i', { classes: this.classes(css.addon, iconCss.icon, iconCss.downIcon), + role: 'presentation', 'aria-hidden': 'true' + }); + } + render(): DNode { let { describedBy, @@ -87,12 +111,7 @@ export default class Button extends ButtonBase { } return v('button', { - classes: this.classes( - css.root, - disabled ? css.disabled : null, - popup ? css.popup : null, - pressed ? css.pressed : null - ), + classes: this.classes(css.root, ...this.getModifierClasses()), disabled, id, name, @@ -115,10 +134,8 @@ export default class Button extends ButtonBase { 'aria-pressed': typeof pressed === 'boolean' ? pressed.toString() : null, 'aria-describedby': describedBy }, [ - ...this.children, - popup ? v('i', { classes: this.classes(css.addon, iconCss.icon, iconCss.downIcon), - role: 'presentation', 'aria-hidden': 'true' - }) : null + ...this.getContent(), + popup ? this.renderPopupIcon() : null ]); } } diff --git a/src/button/tests/functional/Button.ts b/src/button/tests/functional/Button.ts index 21fd7147c9..c197436aca 100644 --- a/src/button/tests/functional/Button.ts +++ b/src/button/tests/functional/Button.ts @@ -10,6 +10,8 @@ function getPage(remote: Remote) { .setFindTimeout(5000); } +const DELAY = 750; + registerSuite('Button', { 'button should be visible'() { return getPage(this.remote) @@ -49,6 +51,7 @@ registerSuite('Button', { assert.isNull(pressed, 'Initial state should be null'); }) .click() + .sleep(DELAY) .end() .findByCssSelector(`#example-4 .${css.root}`) .getAttribute('aria-pressed') diff --git a/src/calendar/Calendar.ts b/src/calendar/Calendar.ts index e7f0435022..03f12e1702 100644 --- a/src/calendar/Calendar.ts +++ b/src/calendar/Calendar.ts @@ -1,11 +1,11 @@ import { WidgetBase } from '@dojo/widget-core/WidgetBase'; import { ThemeableMixin, ThemeableProperties, theme } from '@dojo/widget-core/mixins/Themeable'; import { v, w } from '@dojo/widget-core/d'; -import { DNode, Constructor } from '@dojo/widget-core/interfaces'; +import { DNode } from '@dojo/widget-core/interfaces'; import uuid from '@dojo/core/uuid'; import { Keys } from '../common/util'; import { CalendarMessages } from './DatePicker'; -import DatePicker from './DatePicker'; +import DatePicker, { Paging } from './DatePicker'; import CalendarCell from './CalendarCell'; import * as css from './styles/calendar.m.css'; import * as baseCss from '../common/styles/base.m.css'; @@ -16,7 +16,6 @@ import * as iconCss from '../common/styles/icons.m.css'; * * Properties that can be set on a Calendar component * - * @property CustomDateCell Custom widget constructor for the date cell. Should use CalendarCell as a base. * @property labels Customize or internationalize accessible text for the Calendar widget * @property month Set the currently displayed month, 0-based * @property monthNames Customize or internationalize full month names and abbreviations @@ -30,7 +29,6 @@ import * as iconCss from '../common/styles/icons.m.css'; * @property onDateSelect Function called when the user selects a date */ export interface CalendarProperties extends ThemeableProperties { - CustomDateCell?: Constructor; labels?: CalendarMessages; month?: number; monthNames?: { short: string; long: string; }[]; @@ -234,7 +232,6 @@ export default class Calendar extends CalendarBase { month, year } = this._getMonthYear(); - const { theme = {}, CustomDateCell = CalendarCell } = this.properties; const currentMonthLength = this._getMonthLength(month, year); const previousMonthLength = this._getMonthLength(month - 1, year); @@ -279,19 +276,9 @@ export default class Calendar extends CalendarBase { isSelectedDay = false; } - days.push(w(CustomDateCell, { - key: `date-${week * 7 + i}`, - callFocus: this._callDateFocus && isCurrentMonth && date === this._focusedDay, - date, - disabled: !isCurrentMonth, - focusable: isCurrentMonth && date === this._focusedDay, - selected: isSelectedDay, - theme, - today: isCurrentMonth && dateString === todayString, - onClick: this._onDateClick, - onFocusCalled: this._onDateFocusCalled, - onKeyDown: this._onDateKeyDown - })); + const isToday = isCurrentMonth && dateString === todayString; + + days.push(this.renderDateCell(date, week * 7 + i, isSelectedDay, isCurrentMonth, isToday)); } weeks.push(v('tr', days)); @@ -300,19 +287,30 @@ export default class Calendar extends CalendarBase { return weeks; } - private _renderWeekdayCell(day: { short: string; long: string; }): DNode { - const { renderWeekdayCell } = this.properties; - return renderWeekdayCell ? renderWeekdayCell(day) : v('abbr', { title: day.long }, [ day.short ]); + protected renderDateCell(date: number, index: number, selected: boolean, currentMonth: boolean, today: boolean): DNode { + const { theme = {} } = this.properties; + + return w(CalendarCell, { + key: `date-${index}`, + callFocus: this._callDateFocus && currentMonth && date === this._focusedDay, + date, + disabled: !currentMonth, + focusable: currentMonth && date === this._focusedDay, + selected, + theme, + today, + onClick: this._onDateClick, + onFocusCalled: this._onDateFocusCalled, + onKeyDown: this._onDateKeyDown + }); } - protected render(): DNode { + protected renderDatePicker(): DNode { const { labels = DEFAULT_LABELS, monthNames = DEFAULT_MONTHS, renderMonthLabel, - selectedDate, theme = {}, - weekdayNames = DEFAULT_WEEKDAYS, onMonthChange, onYearChange } = this.properties; @@ -321,6 +319,51 @@ export default class Calendar extends CalendarBase { year } = this._getMonthYear(); + return w(DatePicker, { + key: 'date-picker', + labelId: this._monthLabelId, + labels, + month, + monthNames, + renderMonthLabel, + theme, + year, + onPopupChange: (open: boolean) => { + this._popupOpen = open; + }, + onRequestMonthChange: (requestMonth: number) => { + onMonthChange && onMonthChange(requestMonth); + }, + onRequestYearChange: (requestYear: number) => { + onYearChange && onYearChange(requestYear); + } + }); + } + + protected renderPagingButtonContent(type: Paging): DNode[] { + const { labels = DEFAULT_LABELS } = this.properties; + const iconClass = type === Paging.next ? iconCss.rightIcon : iconCss.leftIcon; + const labelText = type === Paging.next ? labels.nextMonth : labels.previousMonth; + + return [ + v('i', { classes: this.classes(iconCss.icon, iconClass), + role: 'presentation', 'aria-hidden': 'true' + }), + v('span', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labelText ]) + ]; + } + + protected renderWeekdayCell(day: { short: string; long: string; }): DNode { + const { renderWeekdayCell } = this.properties; + return renderWeekdayCell ? renderWeekdayCell(day) : v('abbr', { title: day.long }, [ day.short ]); + } + + protected render(): DNode { + const { + selectedDate, + weekdayNames = DEFAULT_WEEKDAYS + } = this.properties; + // Calendar Weekday array const weekdays = []; for (const weekday in weekdayNames) { @@ -328,31 +371,13 @@ export default class Calendar extends CalendarBase { role: 'columnheader', classes: this.classes(css.weekday) }, [ - this._renderWeekdayCell(weekdayNames[weekday]) + this.renderWeekdayCell(weekdayNames[weekday]) ])); } return v('div', { classes: this.classes(css.root) }, [ // header - w(DatePicker, { - key: 'date-picker', - labelId: this._monthLabelId, - labels, - month, - monthNames, - renderMonthLabel, - theme, - year, - onPopupChange: (open: boolean) => { - this._popupOpen = open; - }, - onRequestMonthChange: (requestMonth: number) => { - onMonthChange && onMonthChange(requestMonth); - }, - onRequestYearChange: (requestYear: number) => { - onYearChange && onYearChange(requestYear); - } - }), + this.renderDatePicker(), // date table v('table', { cellspacing: '0', @@ -374,22 +399,12 @@ export default class Calendar extends CalendarBase { classes: this.classes(css.previous), tabIndex: this._popupOpen ? -1 : 0, onclick: this._onMonthPageDown - }, [ - v('i', { classes: this.classes(iconCss.icon, iconCss.leftIcon), - role: 'presentation', 'aria-hidden': 'true' - }), - v('span', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labels.previousMonth ]) - ]), + }, this.renderPagingButtonContent(Paging.previous)), v('button', { classes: this.classes(css.next), tabIndex: this._popupOpen ? -1 : 0, onclick: this._onMonthPageUp - }, [ - v('i', { classes: this.classes(iconCss.icon, iconCss.rightIcon), - role: 'presentation', 'aria-hidden': 'true' - }), - v('span', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labels.nextMonth ]) - ]) + }, this.renderPagingButtonContent(Paging.next)) ]) ]); } diff --git a/src/calendar/CalendarCell.ts b/src/calendar/CalendarCell.ts index 8d65dcf3e3..5ed28800e2 100644 --- a/src/calendar/CalendarCell.ts +++ b/src/calendar/CalendarCell.ts @@ -65,28 +65,33 @@ export default class CalendarCell extends CalendarCellBase { onPopupChange && onPopupChange(this._getPopupState()); } - private _renderMonthRadios() { + protected renderControlsTrigger(type: Controls): DNode { + const { + month, + monthNames, + year + } = this.properties; + + const content = type === Controls.month ? monthNames[month].long : `${year}`; + const open = type === Controls.month ? this._monthPopupOpen : this._yearPopupOpen; + const onclick = type === Controls.month ? this._onMonthButtonClick : this._onYearButtonClick; + + return v('button', { + key: `${type}-button`, + 'aria-controls': `${this._idBase}_${type}_dialog`, + 'aria-expanded': `${open}`, + 'aria-haspopup': 'true', + id: `${this._idBase}_${type}_button`, + classes: this.classes( + (css as any)[`${type}Trigger`], + open ? (css as any)[`${type}TriggerActive`] : null + ), + role: 'menuitem', + onclick + }, [ content ]); + } + + protected renderMonthLabel(month: number, year: number): DNode { + const { monthNames, renderMonthLabel } = this.properties; + return renderMonthLabel ? renderMonthLabel(month, year) : `${monthNames[month].long} ${year}`; + } + + protected renderMonthRadios(): DNode[] { const { month } = this.properties; return this.properties.monthNames.map((monthName, i) => v('label', { @@ -215,7 +262,20 @@ export default class DatePicker extends DatePickerBase { ])); } - private _renderYearRadios() { + protected renderPagingButtonContent(type: Paging): DNode[] { + const { labels } = this.properties; + const iconClass = type === Paging.next ? iconCss.rightIcon : iconCss.leftIcon; + const labelText = type === Paging.next ? labels.nextMonth : labels.previousMonth; + + return [ + v('i', { classes: this.classes(iconCss.icon, iconClass), + role: 'presentation', 'aria-hidden': 'true' + }), + v('span', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labelText ]) + ]; + } + + protected renderYearRadios(): DNode[] { const { year } = this.properties; const radios = []; @@ -244,17 +304,11 @@ export default class DatePicker extends DatePickerBase { return radios; } - private _renderMonthLabel(month: number, year: number) { - const { monthNames, renderMonthLabel } = this.properties; - return renderMonthLabel ? renderMonthLabel(month, year) : `${monthNames[month].long} ${year}`; - } - protected render(): DNode { const { labelId = `${this._idBase}_label`, labels, month, - monthNames, year } = this.properties; @@ -271,37 +325,13 @@ export default class DatePicker extends DatePickerBase { classes: this.classes().fixed(baseCss.visuallyHidden), 'aria-live': 'polite', 'aria-atomic': 'false' - }, [ this._renderMonthLabel(month, year) ]), + }, [ this.renderMonthLabel(month, year) ]), // month trigger - v('button', { - key: 'month-button', - 'aria-controls': `${this._idBase}_month_dialog`, - 'aria-expanded': `${this._monthPopupOpen}`, - 'aria-haspopup': 'true', - id: `${this._idBase}_month_button`, - classes: this.classes( - css.monthTrigger, - this._monthPopupOpen ? css.monthTriggerActive : null - ), - role: 'menuitem', - onclick: this._onMonthButtonClick - }, [ monthNames[month].long ]), + this.renderControlsTrigger(Controls.month), // year trigger - v('button', { - key: 'year-button', - 'aria-controls': `${this._idBase}_year_dialog`, - 'aria-expanded': `${this._yearPopupOpen}`, - 'aria-haspopup': 'true', - id: `${this._idBase}_year_button`, - classes: this.classes( - css.yearTrigger, - this._yearPopupOpen ? css.yearTriggerActive : null - ), - role: 'menuitem', - onclick: this._onYearButtonClick - }, [ `${ year }` ]) + this.renderControlsTrigger(Controls.year) ]), // month grid @@ -318,7 +348,7 @@ export default class DatePicker extends DatePickerBase { onkeydown: this._onPopupKeyDown }, [ v('legend', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labels.chooseMonth ]), - ...this._renderMonthRadios() + ...this.renderMonthRadios() ]) ]), @@ -336,7 +366,7 @@ export default class DatePicker extends DatePickerBase { onkeydown: this._onPopupKeyDown }, [ v('legend', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labels.chooseYear ]), - ...this._renderYearRadios() + ...this.renderYearRadios() ]), v('div', { classes: this.classes(css.controls) @@ -345,22 +375,12 @@ export default class DatePicker extends DatePickerBase { classes: this.classes(css.previous), tabIndex: this._yearPopupOpen ? 0 : -1, onclick: this._onYearPageDown - }, [ - v('i', { classes: this.classes(iconCss.icon, iconCss.leftIcon), - role: 'presentation', 'aria-hidden': 'true' - }), - v('span', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labels.previousMonth ]) - ]), + }, this.renderPagingButtonContent(Paging.previous)), v('button', { classes: this.classes(css.next), tabIndex: this._yearPopupOpen ? 0 : -1, onclick: this._onYearPageUp - }, [ - v('i', { classes: this.classes(iconCss.icon, iconCss.rightIcon), - role: 'presentation', 'aria-hidden': 'true' - }), - v('span', { classes: this.classes().fixed(baseCss.visuallyHidden) }, [ labels.nextMonth ]) - ]) + }, this.renderPagingButtonContent(Paging.next)) ]) ]) ]); diff --git a/src/calendar/createCalendarElement.ts b/src/calendar/createCalendarElement.ts index 48feb86390..b438cc446e 100644 --- a/src/calendar/createCalendarElement.ts +++ b/src/calendar/createCalendarElement.ts @@ -24,7 +24,6 @@ export default function createCalendarElement(): CustomElementDescriptor { } ], properties: [ - { propertyName: 'CustomDateCell' }, { propertyName: 'labels' }, { propertyName: 'monthNames' }, { propertyName: 'weekdayNames' }, diff --git a/src/calendar/tests/unit/Calendar.ts b/src/calendar/tests/unit/Calendar.ts index 5ba7ac4cfb..414ba21c5f 100644 --- a/src/calendar/tests/unit/Calendar.ts +++ b/src/calendar/tests/unit/Calendar.ts @@ -186,7 +186,6 @@ registerSuite('Calendar', { 'Renders with custom properties'() { widget.setProperties({ - CustomDateCell: CalendarCell, labels: DEFAULT_LABELS, month: testDate.getMonth(), monthNames: DEFAULT_MONTHS, @@ -211,7 +210,6 @@ registerSuite('Calendar', { widget.expectRender(expectedVdom); - class CustomCalendarCell extends CalendarCell {} widget.setProperties({ month: testDate.getMonth(), year: testDate.getFullYear() diff --git a/src/checkbox/Checkbox.ts b/src/checkbox/Checkbox.ts index 75c7eb421e..e674fde2c7 100644 --- a/src/checkbox/Checkbox.ts +++ b/src/checkbox/Checkbox.ts @@ -87,6 +87,28 @@ export default class Checkbox extends CheckboxBase { private _onTouchEnd (event: TouchEvent) { this.properties.onTouchEnd && this.properties.onTouchEnd(event); } private _onTouchCancel (event: TouchEvent) { this.properties.onTouchCancel && this.properties.onTouchCancel(event); } + protected getModifierClasses(): (string | null)[] { + const { + checked = false, + disabled, + invalid, + mode, + readOnly, + required + } = this.properties; + + return [ + mode === Mode.toggle ? css.toggle : null, + checked ? css.checked : null, + disabled ? css.disabled : null, + this._focused ? css.focused : null, + invalid ? css.invalid : null, + invalid === false ? css.valid : null, + readOnly ? css.readonly : null, + required ? css.required : null + ]; + } + protected renderToggle(): DNode[] { const { checked, @@ -117,24 +139,12 @@ export default class Checkbox extends CheckboxBase { disabled, invalid, label, - mode, name, readOnly, required, value } = this.properties; - const stateClasses = [ - mode === Mode.toggle ? css.toggle : null, - checked ? css.checked : null, - disabled ? css.disabled : null, - this._focused ? css.focused : null, - invalid ? css.invalid : null, - invalid === false ? css.valid : null, - readOnly ? css.readonly : null, - required ? css.required : null - ]; - const children = [ v('div', { classes: this.classes(css.inputWrapper) }, [ ...this.renderToggle(), @@ -167,14 +177,14 @@ export default class Checkbox extends CheckboxBase { if (label) { checkboxWidget = w(Label, { - extraClasses: { root: parseLabelClasses(this.classes(css.root, ...stateClasses).get()) }, + extraClasses: { root: parseLabelClasses(this.classes(css.root, ...this.getModifierClasses()).get()) }, label, theme: this.properties.theme }, children); } else { checkboxWidget = v('div', { - classes: this.classes(css.root, ...stateClasses) + classes: this.classes(css.root, ...this.getModifierClasses()) }, children); } diff --git a/src/dialog/Dialog.ts b/src/dialog/Dialog.ts index 881c803b33..72ffb2ed06 100644 --- a/src/dialog/Dialog.ts +++ b/src/dialog/Dialog.ts @@ -83,6 +83,35 @@ export default class Dialog extends DialogBase { })); } + protected getContent(): DNode { + return v('div', { + classes: this.classes(css.content), + key: 'content' + }, this.children); + } + + protected renderCloseIcon(): DNode { + return v('i', { classes: this.classes(iconCss.icon, iconCss.closeIcon), + role: 'presentation', 'aria-hidden': 'true' + }); + } + + protected renderTitle(): DNode { + const { title = '' } = this.properties; + return v('div', { id: this._titleId }, [ title ]); + } + + protected renderUnderlay(): DNode { + const { underlay } = this.properties; + return v('div', { + classes: this.classes(underlay ? css.underlayVisible : null).fixed(css.underlay), + enterAnimation: animations.fadeIn, + exitAnimation: animations.fadeOut, + key: 'underlay', + onclick: this._onUnderlayClick + }); + } + render(): DNode { const { closeable = true, @@ -91,9 +120,7 @@ export default class Dialog extends DialogBase { exitAnimation = animations.fadeOut, onOpen, open = false, - role = 'dialog', - title = '', - underlay + role = 'dialog' } = this.properties; open && !this._wasOpen && onOpen && onOpen(); @@ -103,13 +130,7 @@ export default class Dialog extends DialogBase { return v('div', { classes: this.classes(css.root) }, open ? [ - v('div', { - classes: this.classes(underlay ? css.underlayVisible : null).fixed(css.underlay), - enterAnimation: animations.fadeIn, - exitAnimation: animations.fadeOut, - key: 'underlay', - onclick: this._onUnderlayClick - }), + this.renderUnderlay(), v('div', { 'aria-labelledby': this._titleId, classes: this.classes(css.main), @@ -122,21 +143,16 @@ export default class Dialog extends DialogBase { classes: this.classes(css.title), key: 'title' }, [ - v('div', { id: this._titleId }, [ title ]), + this.renderTitle(), closeable ? v('button', { classes: this.classes(css.close), onclick: this._onCloseClick }, [ closeText, - v('i', { classes: this.classes(iconCss.icon, iconCss.closeIcon), - role: 'presentation', 'aria-hidden': 'true' - }) + this.renderCloseIcon() ]) : null ]), - v('div', { - classes: this.classes(css.content), - key: 'content' - }, this.children) + this.getContent() ]) ] : []); } diff --git a/src/radio/Radio.ts b/src/radio/Radio.ts index 6c87ea828f..201dc8b424 100644 --- a/src/radio/Radio.ts +++ b/src/radio/Radio.ts @@ -74,20 +74,16 @@ export default class Radio extends RadioBase { private _onTouchEnd (event: TouchEvent) { this.properties.onTouchEnd && this.properties.onTouchEnd(event); } private _onTouchCancel (event: TouchEvent) { this.properties.onTouchCancel && this.properties.onTouchCancel(event); } - render(): DNode { + protected getModifierClasses(): (string | null)[] { const { checked = false, - describedBy, disabled, invalid, - label, - name, readOnly, - required, - value + required } = this.properties; - const stateClasses = [ + return [ checked ? css.checked : null, disabled ? css.disabled : null, this._focused ? css.focused : null, @@ -96,6 +92,20 @@ export default class Radio extends RadioBase { readOnly ? css.readonly : null, required ? css.required : null ]; + } + + render(): DNode { + const { + checked = false, + describedBy, + disabled, + invalid, + label, + name, + readOnly, + required, + value + } = this.properties; const radio = v('div', { classes: this.classes(css.inputWrapper) }, [ v('input', { @@ -126,14 +136,14 @@ export default class Radio extends RadioBase { if (label) { radioWidget = w(Label, { - extraClasses: { root: parseLabelClasses(this.classes(css.root, ...stateClasses).get()) }, + extraClasses: { root: parseLabelClasses(this.classes(css.root, ...this.getModifierClasses()).get()) }, label, theme: this.properties.theme }, [ radio ]); } else { radioWidget = v('div', { - classes: this.classes(css.root, ...stateClasses) + classes: this.classes(css.root, ...this.getModifierClasses()) }, [ radio]); } diff --git a/src/slidepane/SlidePane.ts b/src/slidepane/SlidePane.ts index c7d7a8791b..245eaf6a6b 100644 --- a/src/slidepane/SlidePane.ts +++ b/src/slidepane/SlidePane.ts @@ -172,51 +172,96 @@ export default class SlidePane extends SlidePaneBase { } } - render(): DNode { + protected getContent(): DNode { + return v('div', { classes: this.classes(css.content) }, this.children); + } + + protected getStyles(): { [key: string]: string | null } { const { align = Align.left, - closeText = 'close pane', - onOpen, open = false, - title = '', - underlay = false, width = DEFAULT_WIDTH } = this.properties; + let translate = ''; + const translateAxis = this.plane === Plane.x ? 'X' : 'Y'; + + // If pane is closing because of swipe + if (!open && this._wasOpen && this._transform) { + translate = align === Align.left || align === Align.top ? `-${this._transform}` : `${this._transform}`; + } + + return { + transform: translate ? `translate${translateAxis}(${translate}%)` : '', + width: this.plane === Plane.x ? `${ width }px` : null, + height: this.plane === Plane.y ? `${ width }px` : null + }; + } + + protected getFixedModifierClasses(): (string | null)[] { + const { + align = Align.left, + open = false + } = this.properties; const alignCss: {[key: string]: any} = css; - const contentClasses = [ - css.pane, + return [ + open ? css.openFixed : null, + alignCss[`${align}Fixed`], + this._slideIn || (open && !this._wasOpen) ? css.slideInFixed : null, + !open && this._wasOpen ? css.slideOutFixed : null + ]; + } + + protected getModifierClasses(): (string | null)[] { + const { + align = Align.left, + open = false + } = this.properties; + const alignCss: {[key: string]: any} = css; + + return [ alignCss[align], open ? css.open : null, this._slideIn || (open && !this._wasOpen) ? css.slideIn : null, !open && this._wasOpen ? css.slideOut : null ]; + } - const fixedContentClasses = [ - css.paneFixed, - open ? css.openFixed : null, - alignCss[`${align}Fixed`], - this._slideIn || (open && !this._wasOpen) ? css.slideInFixed : null, - !open && this._wasOpen ? css.slideOutFixed : null - ]; + protected renderCloseIcon(): DNode { + return v('i', { classes: this.classes(iconCss.icon, iconCss.closeIcon), + role: 'presentation', 'aria-hidden': 'true' + }); + } - const contentStyles: {[key: string]: any} = { - transform: '', - width: this.plane === Plane.x ? `${ width }px` : null, - height: this.plane === Plane.y ? `${ width }px` : null - }; + protected renderTitle(): DNode { + const { title = '' } = this.properties; + return v('div', { id: this._titleId }, [ title ]); + } - if (!open && this._wasOpen && this._transform) { - // If pane is closing because of swipe - if (this.plane === Plane.x) { - contentStyles['transform'] = `translateX(${ align === Align.left ? '-' : '' }${ this._transform }%)`; - } - else { - contentStyles['transform'] = `translateY(${ align === Align.top ? '-' : '' }${ this._transform }%)`; - } - } - else if (this._slideIn && this._content) { + protected renderUnderlay(): DNode { + const { underlay = false } = this.properties; + return v('div', { + classes: this.classes(underlay ? css.underlayVisible : null).fixed(css.underlay), + enterAnimation: animations.fadeIn, + exitAnimation: animations.fadeOut, + key: 'underlay' + }); + } + + render(): DNode { + const { + closeText = 'close pane', + onOpen, + open = false, + title = '' + } = this.properties; + + const contentStyles = this.getStyles(); + const contentClasses = this.getModifierClasses(); + const fixedContentClasses = this.getFixedModifierClasses(); + + if (this._slideIn && this._content) { this._content.style.transform = ''; } @@ -234,33 +279,26 @@ export default class SlidePane extends SlidePaneBase { ontouchmove: this._onSwipeMove, ontouchstart: this._onSwipeStart }, [ - open ? v('div', { - classes: this.classes(underlay ? css.underlayVisible : null).fixed(css.underlay), - enterAnimation: animations.fadeIn, - exitAnimation: animations.fadeOut, - key: 'underlay' - }) : null, + open ? this.renderUnderlay() : null, v('div', { key: 'content', - classes: this.classes(...contentClasses).fixed(...fixedContentClasses), + classes: this.classes(css.pane, ...contentClasses).fixed(css.paneFixed, ...fixedContentClasses), styles: contentStyles }, [ title ? v('div', { classes: this.classes(css.title), key: 'title' }, [ - v('div', { id: this._titleId }, [ title ]), + this.renderTitle(), v('button', { classes: this.classes(css.close), onclick: this._onCloseClick }, [ closeText, - v('i', { classes: this.classes(iconCss.icon, iconCss.closeIcon), - role: 'presentation', 'aria-hidden': 'true' - }) + this.renderCloseIcon() ]) ]) : null, - v('div', { classes: this.classes(css.content) }, this.children) + this.getContent() ]) ]); } diff --git a/src/slider/Slider.ts b/src/slider/Slider.ts index bf1d58b45a..bafa654c7c 100644 --- a/src/slider/Slider.ts +++ b/src/slider/Slider.ts @@ -91,31 +91,16 @@ export default class Slider extends SliderBase { private _onTouchEnd (event: TouchEvent) { this.properties.onTouchEnd && this.properties.onTouchEnd(event); } private _onTouchCancel (event: TouchEvent) { this.properties.onTouchCancel && this.properties.onTouchCancel(event); } - render(): DNode { + protected getModifierClasses(): (string | null)[] { const { - describedBy, disabled, invalid, - label, - max = 100, - min = 0, - name, - output, - outputIsTooltip = false, readOnly, required, - step = 1, - vertical = false, - verticalHeight = '200px' + vertical = false } = this.properties; - let { - value = min - } = this.properties; - - value = value > max ? max : value; - value = value < min ? min : value; - const stateClasses = [ + return [ disabled ? css.disabled : null, invalid ? css.invalid : null, invalid === false ? css.valid : null, @@ -123,10 +108,37 @@ export default class Slider extends SliderBase { required ? css.required : null, vertical ? css.vertical : null ]; + } - const percentValue = (value - min) / (max - min) * 100; + protected renderControls(percentValue: number): DNode { + const { + vertical = false, + verticalHeight = '200px' + } = this.properties; + + return v('div', { + classes: this.classes(css.track).fixed(css.trackFixed), + 'aria-hidden': 'true', + styles: vertical ? { width: verticalHeight } : {} + }, [ + v('span', { + classes: this.classes(css.fill).fixed(css.fillFixed), + styles: { width: `${percentValue}%` } + }), + v('span', { + classes: this.classes(css.thumb).fixed(css.thumbFixed), + styles: { left: `${percentValue}%` } + }) + ]); + } + + protected renderOutput(value: number, percentValue: number): DNode { + const { + output, + outputIsTooltip = false, + vertical = false + } = this.properties; - // custom output node const outputNode = output ? output(value) : `${value}`; // output styles @@ -135,6 +147,37 @@ export default class Slider extends SliderBase { outputStyles = vertical ? { top: `${100 - percentValue}%` } : { left: `${percentValue}%` }; } + return v('output', { + classes: this.classes(css.output, outputIsTooltip ? css.outputTooltip : null), + for: `${this._inputId}`, + styles: outputStyles + }, [ outputNode ]); + } + + render(): DNode { + const { + describedBy, + disabled, + invalid, + label, + max = 100, + min = 0, + name, + readOnly, + required, + step = 1, + vertical = false, + verticalHeight = '200px' + } = this.properties; + let { + value = min + } = this.properties; + + value = value > max ? max : value; + value = value < min ? min : value; + + const percentValue = (value - min) / (max - min) * 100; + const slider = v('div', { classes: this.classes(css.inputWrapper).fixed(css.inputWrapperFixed), styles: vertical ? { height: verticalHeight } : {} @@ -169,39 +212,22 @@ export default class Slider extends SliderBase { ontouchend: this._onTouchEnd, ontouchcancel: this._onTouchCancel }), - v('div', { - classes: this.classes(css.track).fixed(css.trackFixed), - 'aria-hidden': 'true', - styles: vertical ? { width: verticalHeight } : {} - }, [ - v('span', { - classes: this.classes(css.fill).fixed(css.fillFixed), - styles: { width: `${percentValue}%` } - }), - v('span', { - classes: this.classes(css.thumb).fixed(css.thumbFixed), - styles: { left: `${percentValue}%` } - }) - ]), - v('output', { - classes: this.classes(css.output, outputIsTooltip ? css.outputTooltip : null), - for: `${this._inputId}`, - styles: outputStyles - }, [ outputNode ]) + this.renderControls(percentValue), + this.renderOutput(value, percentValue) ]); let sliderWidget; if (label) { sliderWidget = w(Label, { - extraClasses: { root: parseLabelClasses(this.classes(css.root, ...stateClasses).fixed(css.rootFixed)()) }, + extraClasses: { root: parseLabelClasses(this.classes(css.root, ...this.getModifierClasses()).fixed(css.rootFixed)()) }, label, theme: this.properties.theme }, [ slider ]); } else { sliderWidget = v('div', { - classes: this.classes(css.root, ...stateClasses).fixed(css.rootFixed) + classes: this.classes(css.root, ...this.getModifierClasses()).fixed(css.rootFixed) }, [ slider ]); } diff --git a/src/splitpane/SplitPane.ts b/src/splitpane/SplitPane.ts index 5423611d2e..5a26656aad 100644 --- a/src/splitpane/SplitPane.ts +++ b/src/splitpane/SplitPane.ts @@ -128,6 +128,21 @@ export default class SplitPane extends SplitPaneBase { this._lastSize = undefined; } + protected getPaneContent(content: DNode): DNode[] { + return [ content ]; + } + + protected getPaneStyles(): {[key: string]: string} { + const { + direction = Direction.row, + size = DEFAULT_SIZE + } = this.properties; + const styles: {[key: string]: string} = {}; + + styles[direction === Direction.row ? 'width' : 'height'] = `${size}px`; + return styles; + } + protected onElementCreated(element: HTMLElement, key: string) { if (key === 'root') { this._root = element; @@ -141,13 +156,9 @@ export default class SplitPane extends SplitPaneBase { const { direction = Direction.row, leading = null, - size = DEFAULT_SIZE, trailing = null } = this.properties; - const styles: {[key: string]: string} = {}; - styles[direction === Direction.row ? 'width' : 'height'] = `${size}px`; - return v('div', { classes: this.classes( css.root, @@ -165,8 +176,8 @@ export default class SplitPane extends SplitPaneBase { css.leadingFixed ), key: 'leading', - styles - }, [ leading ]), + styles: this.getPaneStyles() + }, this.getPaneContent(leading)), v('div', { classes: this.classes( css.divider @@ -185,7 +196,7 @@ export default class SplitPane extends SplitPaneBase { css.trailingFixed ), key: 'trailing' - }, [ trailing ]) + }, this.getPaneContent(trailing)) ]); } } diff --git a/src/tabcontroller/TabButton.ts b/src/tabcontroller/TabButton.ts index 56e4061b19..01064739bb 100644 --- a/src/tabcontroller/TabButton.ts +++ b/src/tabcontroller/TabButton.ts @@ -130,6 +130,28 @@ export default class TabButton extends TabButtonBase { } } + protected getContent(): DNode[] { + const { active, closeable } = this.properties; + + return [ + ...this.children, + closeable ? v('button', { + tabIndex: active ? 0 : -1, + classes: this.classes(css.close), + innerHTML: 'close tab', + onclick: this._onCloseClick + }) : null + ]; + } + + protected getModifierClasses(): (string | null)[] { + const { active, disabled } = this.properties; + return [ + active ? css.activeTabButton : null, + disabled ? css.disabledTabButton : null + ]; + } + protected onElementCreated(element: HTMLElement, key: string) { key === 'tab-button' && this._callFocus(element); } @@ -141,36 +163,22 @@ export default class TabButton extends TabButtonBase { render(): DNode { const { active, - closeable, controls, disabled, id } = this.properties; - const children = closeable ? this.children.concat([ - v('button', { - tabIndex: active ? 0 : -1, - classes: this.classes(css.close), - innerHTML: 'close tab', - onclick: this._onCloseClick - }) - ]) : this.children; - return v('div', { 'aria-controls': controls, 'aria-disabled': disabled ? 'true' : 'false', 'aria-selected': active ? 'true' : 'false', - classes: this.classes( - css.tabButton, - active ? css.activeTabButton : null, - disabled ? css.disabledTabButton : null - ), + classes: this.classes(css.tabButton, ...this.getModifierClasses()), id, key: 'tab-button', onclick: this._onClick, onkeydown: this._onKeyDown, role: 'tab', tabIndex: active ? 0 : -1 - }, children); + }, this.getContent()); } } diff --git a/src/tabcontroller/TabController.ts b/src/tabcontroller/TabController.ts index 73b443b49b..77aaaeb53c 100644 --- a/src/tabcontroller/TabController.ts +++ b/src/tabcontroller/TabController.ts @@ -71,7 +71,46 @@ export default class TabController extends TabControllerBase Boolean(result.properties.disabled))) { + return null; + } + + function nextIndex(index: number) { + if (backwards) { + return (tabs.length + (index - 1)) % tabs.length; + } + return (index + 1) % tabs.length; + } + + let i = !tabs[currentIndex] ? tabs.length - 1 : currentIndex; + + while (tabs[i].properties.disabled) { + i = nextIndex(i); + } + + return i; + } + + protected closeIndex(index: number) { + const { onRequestTabClose } = this.properties; + const key = this._tabs[index].properties.key; + this._callTabFocus = true; + + onRequestTabClose && onRequestTabClose(index, key); + } + + protected renderButtonContent(label?: DNode): DNode[] { + return [ label || null ]; + } + + protected renderTabButtons(): DNode[] { return this._tabs.map((tab, i) => { const { closeable, @@ -100,13 +139,11 @@ export default class TabController extends TabControllerBase Boolean(result.properties.disabled))) { - return null; - } - - function nextIndex(index: number) { - if (backwards) { - return (tabs.length + (index - 1)) % tabs.length; - } - return (index + 1) % tabs.length; - } - - let i = !tabs[currentIndex] ? tabs.length - 1 : currentIndex; - - while (tabs[i].properties.disabled) { - i = nextIndex(i); - } - - return i; - } - - protected closeIndex(index: number) { - const { onRequestTabClose } = this.properties; - const key = this._tabs[index].properties.key; - this._callTabFocus = true; - - onRequestTabClose && onRequestTabClose(index, key); - } - protected selectFirstIndex() { this.selectIndex(0, true); } @@ -195,7 +197,7 @@ export default class TabController extends TabControllerBase { private _onTouchEnd (event: TouchEvent) { this.properties.onTouchEnd && this.properties.onTouchEnd(event); } private _onTouchCancel (event: TouchEvent) { this.properties.onTouchCancel && this.properties.onTouchCancel(event); } + protected getModifierClasses(): (string | null)[] { + const { + disabled, + invalid, + readOnly, + required + } = this.properties; + return [ + disabled ? css.disabled : null, + invalid ? css.invalid : null, + invalid === false ? css.valid : null, + readOnly ? css.readonly : null, + required ? css.required : null + ]; + } + render(): DNode { const { columns, @@ -104,14 +120,6 @@ export default class Textarea extends TextareaBase { wrapText } = this.properties; - const stateClasses = [ - disabled ? css.disabled : null, - invalid ? css.invalid : null, - invalid === false ? css.valid : null, - readOnly ? css.readonly : null, - required ? css.required : null - ]; - const textarea = v('div', { classes: this.classes(css.inputWrapper) }, [ v('textarea', { classes: this.classes(css.input), @@ -149,14 +157,14 @@ export default class Textarea extends TextareaBase { if (label) { textareaWidget = w(Label, { - extraClasses: { root: parseLabelClasses(this.classes(css.root, ...stateClasses)()) }, + extraClasses: { root: parseLabelClasses(this.classes(css.root, ...this.getModifierClasses())()) }, label, theme: this.properties.theme }, [ textarea ]); } else { textareaWidget = v('div', { - classes: this.classes(css.root, ...stateClasses) + classes: this.classes(css.root, ...this.getModifierClasses()) }, [ textarea ]); } diff --git a/src/textinput/TextInput.ts b/src/textinput/TextInput.ts index 7eb2303c8c..4607217e5f 100644 --- a/src/textinput/TextInput.ts +++ b/src/textinput/TextInput.ts @@ -86,6 +86,22 @@ export default class TextInput extends TextInputBase { private _onTouchEnd (event: TouchEvent) { this.properties.onTouchEnd && this.properties.onTouchEnd(event); } private _onTouchCancel (event: TouchEvent) { this.properties.onTouchCancel && this.properties.onTouchCancel(event); } + protected getModifierClasses(): (string | null)[] { + const { + disabled, + invalid, + readOnly, + required + } = this.properties; + return [ + disabled ? css.disabled : null, + invalid ? css.invalid : null, + invalid === false ? css.valid : null, + readOnly ? css.readonly : null, + required ? css.required : null + ]; + } + render(): DNode { const { controls, @@ -103,14 +119,6 @@ export default class TextInput extends TextInputBase { value } = this.properties; - const stateClasses = [ - disabled ? css.disabled : null, - invalid ? css.invalid : null, - invalid === false ? css.valid : null, - readOnly ? css.readonly : null, - required ? css.required : null - ]; - const textinput = v('div', { classes: this.classes(css.inputWrapper) }, [ v('input', { classes: this.classes(css.input), @@ -147,14 +155,14 @@ export default class TextInput extends TextInputBase { if (label) { textinputWidget = w(Label, { - extraClasses: { root: parseLabelClasses(this.classes(css.root, ...stateClasses)()) }, + extraClasses: { root: parseLabelClasses(this.classes(css.root, ...this.getModifierClasses())()) }, label, theme: this.properties.theme }, [ textinput ]); } else { textinputWidget = v('div', { - classes: this.classes(css.root, ...stateClasses) + classes: this.classes(css.root, ...this.getModifierClasses()) }, [ textinput ]); } diff --git a/src/themes/dojo/titlePane.m.css b/src/themes/dojo/titlePane.m.css index 1e63c47f1c..6a9b3afb60 100644 --- a/src/themes/dojo/titlePane.m.css +++ b/src/themes/dojo/titlePane.m.css @@ -16,8 +16,11 @@ background-color: var(--color-background); border: var(--border-width) solid var(--color-border); color: var(--color-text-faded); + cursor: pointer; + font-size: var(--font-size-base); padding: var(--grid-base) var(--grid-base) var(--grid-base) calc(var(--grid-base) * 4); position: relative; + width: 100%; z-index: 1; } @@ -47,7 +50,7 @@ .arrow { position: absolute; left: 8px; - top: 12px; + top: 10px; } .open .arrow { diff --git a/src/timepicker/TimePicker.ts b/src/timepicker/TimePicker.ts index 0ef59786a4..b4b828fe0a 100644 --- a/src/timepicker/TimePicker.ts +++ b/src/timepicker/TimePicker.ts @@ -10,6 +10,10 @@ import ComboBox from '../combobox/ComboBox'; import Label, { LabelOptions, parseLabelClasses } from '../label/Label'; import { TextInputProperties } from '../textinput/TextInput'; +interface FocusInputEvent extends FocusEvent { + target: HTMLInputElement; +} + /** * @type TimePickerProperties * @@ -166,41 +170,50 @@ export const TimePickerBase = ThemeableMixin(WidgetBase); export class TimePicker extends TimePickerBase { protected options: TimeUnits[] | null; - render(): DNode { + private _formatUnits(units: TimeUnits): string { + const { step = 60 } = this.properties; + const { hour, minute, second } = units; + + return (step >= 60 ? [ hour, minute ] : [ hour, minute, second ]) + .map(unit => padStart(String(unit), 2, '0')) + .join(':'); + } + + private _getOptionLabel(value: TimeUnits) { + const { getOptionLabel } = this.properties; + const units = parseUnits(value); + return getOptionLabel ? getOptionLabel(units) : this._formatUnits(units); + } + + private _onNativeBlur(event: FocusInputEvent) { + this.properties.onBlur && this.properties.onBlur(event.target.value); + } + + private _onNativeChange(event: FocusInputEvent) { + this.properties.onChange && this.properties.onChange(event.target.value); + } + + private _onNativeFocus(event: FocusInputEvent) { + this.properties.onFocus && this.properties.onFocus(event.target.value); + } + + private _onRequestOptions(value: string) { + this.properties.onRequestOptions && this.properties.onRequestOptions(value, this.getOptions()); + } + + protected getModifierClasses(): (string | null)[] { const { disabled, invalid, - label, readOnly, - required, - useNativeElement + required } = this.properties; - - if (useNativeElement) { - const input = this.renderNativeInput(); - let children: DNode[] = [ input ]; - - if (label) { - children = [ w(Label, { - extraClasses: { root: parseLabelClasses(this.classes( - css.input, - disabled ? css.disabled : null, - invalid ? css.invalid : null, - readOnly ? css.readonly : null, - required ? css.required : null).get()) - }, - label, - theme: this.properties.theme - }, [ input ]) ]; - } - - return v('span', { - classes: this.classes(css.root), - key: 'root' - }, children); - } - - return this.renderCustomInput(); + return [ + disabled ? css.disabled : null, + invalid ? css.invalid : null, + readOnly ? css.readonly : null, + required ? css.required : null + ]; } protected getOptions() { @@ -220,50 +233,6 @@ export class TimePicker extends TimePickerBase { this.options = null; } - protected renderNativeInput(): DNode { - const { - disabled, - end, - inputProperties, - invalid, - name, - readOnly, - required, - start, - step, - value - } = this.properties; - - const classes = [ - css.input, - disabled ? css.disabled : null, - invalid ? css.invalid : null, - readOnly ? css.readonly : null, - required ? css.required : null - ]; - - return v('input', { - 'aria-describedby': inputProperties && inputProperties.describedBy, - 'aria-invalid': invalid ? 'true' : null, - 'aria-readonly': readOnly ? 'true' : null, - classes: this.classes(...classes), - disabled, - invalid, - key: 'native-input', - max: end, - min: start, - name, - onblur: this._onNativeBlur, - onchange: this._onNativeChange, - onfocus: this._onNativeFocus, - readOnly, - required, - step, - type: 'time', - value - }); - } - protected renderCustomInput(): DNode { const { autoBlur, @@ -314,35 +283,67 @@ export class TimePicker extends TimePickerBase { }); } - private _formatUnits(units: TimeUnits): string { - const { step = 60 } = this.properties; - const { hour, minute, second } = units; + protected renderNativeInput(): DNode { + const { + disabled, + end, + inputProperties, + invalid, + name, + readOnly, + required, + start, + step, + value + } = this.properties; - return (step >= 60 ? [ hour, minute ] : [ hour, minute, second ]) - .map(unit => padStart(String(unit), 2, '0')) - .join(':'); + return v('input', { + 'aria-describedby': inputProperties && inputProperties.describedBy, + 'aria-invalid': invalid ? 'true' : null, + 'aria-readonly': readOnly ? 'true' : null, + classes: this.classes(css.input, ...this.getModifierClasses()), + disabled, + invalid, + key: 'native-input', + max: end, + min: start, + name, + onblur: this._onNativeBlur, + onchange: this._onNativeChange, + onfocus: this._onNativeFocus, + readOnly, + required, + step, + type: 'time', + value + }); } - private _getOptionLabel(value: TimeUnits) { - const { getOptionLabel } = this.properties; - const units = parseUnits(value); - return getOptionLabel ? getOptionLabel(units) : this._formatUnits(units); - } + render(): DNode { + const { + label, + useNativeElement + } = this.properties; - private _onNativeBlur(event: FocusEvent) { - this.properties.onBlur && this.properties.onBlur(( event.target).value); - } + if (useNativeElement) { + const input = this.renderNativeInput(); + let children: DNode[] = [ input ]; - private _onNativeChange(event: FocusEvent) { - this.properties.onChange && this.properties.onChange(( event.target).value); - } + if (label) { + children = [ w(Label, { + extraClasses: { root: parseLabelClasses(this.classes(css.input, ...this.getModifierClasses()).get()) }, + label, + theme: this.properties.theme + }, [ input ]) ]; + } - private _onNativeFocus(event: FocusEvent) { - this.properties.onFocus && this.properties.onFocus(( event.target).value); - } + return v('span', { + classes: this.classes(css.root), + key: 'root' + }, children); + } - private _onRequestOptions(value: string) { - this.properties.onRequestOptions && this.properties.onRequestOptions(value, this.getOptions()); + return this.renderCustomInput(); } } diff --git a/src/titlepane/TitlePane.ts b/src/titlepane/TitlePane.ts index aa80e93c46..1353e5ec1a 100644 --- a/src/titlepane/TitlePane.ts +++ b/src/titlepane/TitlePane.ts @@ -4,8 +4,6 @@ import { theme, ThemeableMixin, ThemeableProperties } from '@dojo/widget-core/mi import { v } from '@dojo/widget-core/d'; import { WidgetBase } from '@dojo/widget-core/WidgetBase'; -import { Keys } from '../common/util'; - import * as css from './styles/titlePane.m.css'; import * as iconCss from '../common/styles/icons.m.css'; @@ -55,14 +53,6 @@ export default class TitlePane extends TitlePaneBase { this._toggle(); } - private _onTitleKeyUp(event: KeyboardEvent) { - const {keyCode } = event; - - if (keyCode === Keys.Enter || keyCode === Keys.Space) { - this._toggle(); - } - } - private _toggle() { const { closeable = true, @@ -92,12 +82,46 @@ export default class TitlePane extends TitlePaneBase { key === 'content' && this._afterRender(element); } + protected getButtonContent(): DNode { + return this.properties.title; + } + + protected getFixedModifierClasses(): (string | null)[] { + const { closeable = true } = this.properties; + return [ + closeable ? css.closeableFixed : null + ]; + } + + protected getModifierClasses(): (string | null)[] { + const { closeable = true } = this.properties; + return [ + closeable ? css.closeable : null + ]; + } + + protected getPaneContent(): DNode[] { + return this.children; + } + + protected renderExpandIcon(): DNode { + const { open = true } = this.properties; + return v('i', { + classes: this.classes( + css.arrow, + iconCss.icon, + open ? iconCss.downIcon : iconCss.rightIcon + ), + role: 'presentation', + 'aria-hidden': 'true' + }); + } + render(): DNode { const { closeable = true, headingLevel, - open = true, - title + open = true } = this.properties; return v('div', { @@ -108,36 +132,19 @@ export default class TitlePane extends TitlePaneBase { }, [ v('div', { 'aria-level': headingLevel ? String(headingLevel) : null, - classes: this.classes( - closeable ? css.closeable : null, - css.title - ).fixed( - closeable ? css.closeableFixed : null, - css.titleFixed - ), - onclick: this._onTitleClick, - onkeyup: this._onTitleKeyUp, + classes: this.classes(css.title, ...this.getModifierClasses()).fixed(css.titleFixed, ...this.getFixedModifierClasses()), role: 'heading' }, [ - v('div', { + v('button', { 'aria-controls': this._contentId, - 'aria-disabled': closeable ? null : 'true', 'aria-expanded': String(open), + disabled: !closeable, classes: this.classes(css.titleButton), id: this._titleId, - role: 'button', - tabIndex: closeable ? 0 : -1 + onclick: this._onTitleClick }, [ - v('i', { - classes: this.classes( - css.arrow, - iconCss.icon, - open ? iconCss.downIcon : iconCss.rightIcon - ), - role: 'presentation', - 'aria-hidden': 'true' - }), - title + this.renderExpandIcon(), + this.getButtonContent() ]) ]), v('div', { @@ -146,7 +153,7 @@ export default class TitlePane extends TitlePaneBase { classes: this.classes(css.content), id: this._contentId, key: 'content' - }, this.children) + }, this.getPaneContent()) ]); } } diff --git a/src/titlepane/tests/functional/TitlePane.ts b/src/titlepane/tests/functional/TitlePane.ts index 5e73925d33..25ebb2e6dc 100644 --- a/src/titlepane/tests/functional/TitlePane.ts +++ b/src/titlepane/tests/functional/TitlePane.ts @@ -32,7 +32,7 @@ registerSuite('TitlePane', { height = size.height; }) .end() - .findByCssSelector('#titlePane2 > div > :first-child') + .findByCssSelector('#titlePane2 button') .click() .end() .sleep(DELAY) diff --git a/src/titlepane/tests/unit/TitlePane.ts b/src/titlepane/tests/unit/TitlePane.ts index be58ed4ff3..cfd0cfafa8 100644 --- a/src/titlepane/tests/unit/TitlePane.ts +++ b/src/titlepane/tests/unit/TitlePane.ts @@ -7,7 +7,6 @@ import { v } from '@dojo/widget-core/d'; import TitlePane, { TitlePaneProperties } from '../../TitlePane'; import * as css from '../../styles/titlePane.m.css'; import * as iconCss from '../../../common/styles/icons.m.css'; -import { Keys } from '../../../common/util'; const isNonEmptyString = compareProperty((value: any) => { return typeof value === 'string' && value.length > 0; @@ -40,18 +39,15 @@ registerSuite('TitlePane', { v('div', { 'aria-level': null, classes: titlePane.classes(css.title, css.titleFixed, css.closeable, css.closeableFixed), - onclick: titlePane.listener, - onkeyup: titlePane.listener, role: 'heading' }, [ - v('div', { + v('button', { 'aria-controls': isNonEmptyString, - 'aria-disabled': null, 'aria-expanded': 'true', classes: titlePane.classes(css.titleButton), + disabled: false, id: isNonEmptyString, - role: 'button', - tabIndex: 0 + onclick: titlePane.listener }, [ v('i', { classes: titlePane.classes( @@ -89,18 +85,15 @@ registerSuite('TitlePane', { v('div', { 'aria-level': '5', classes: titlePane.classes(css.title, css.titleFixed), - onclick: titlePane.listener, - onkeyup: titlePane.listener, role: 'heading' }, [ - v('div', { + v('button', { 'aria-controls': isNonEmptyString, - 'aria-disabled': 'true', 'aria-expanded': 'false', classes: titlePane.classes(css.titleButton), + disabled: true, id: isNonEmptyString, - role: 'button', - tabIndex: -1 + onclick: titlePane.listener }, [ v('i', { classes: titlePane.classes( @@ -135,7 +128,7 @@ registerSuite('TitlePane', { }); titlePane.sendEvent('click', { - selector: `.${css.title}` + selector: `.${css.titleButton}` }); assert.isTrue(called, 'onRequestClose should be called on title click'); }, @@ -152,7 +145,7 @@ registerSuite('TitlePane', { }); titlePane.sendEvent('click', { - selector: `.${css.title}` + selector: `.${css.titleButton}` }); assert.isTrue(called, 'onRequestOpen should be called on title click'); }, @@ -169,7 +162,7 @@ registerSuite('TitlePane', { }); titlePane.getRender(); titlePane.sendEvent('click', { - selector: `.${css.title}` + selector: `.${css.titleButton}` }); titlePane.setProperties({ @@ -181,119 +174,10 @@ registerSuite('TitlePane', { }); titlePane.getRender(); titlePane.sendEvent('click', { - selector: `.${css.title}` + selector: `.${css.titleButton}` }); assert.strictEqual(called, 1, 'onRequestClose should only becalled once'); - }, - - 'can not open pane on keyup'() { - let called = 0; - titlePane.setProperties({ - closeable: false, - open: true, - onRequestClose() { - called++; - }, - title: 'test' - }); - titlePane.getRender(); - titlePane.sendEvent('keyup', { - eventInit: { keyCode: Keys.Enter }, - selector: `.${css.title}` - }); - - titlePane.setProperties({ - open: true, - onRequestClose() { - called++; - }, - title: 'test' - }); - titlePane.getRender(); - titlePane.sendEvent('keyup', { - eventInit: { keyCode: Keys.Enter }, - selector: `.${css.title}` - }); - - assert.strictEqual(called, 1, 'onRequestClose should only becalled once'); - }, - - 'open on keyup'() { - let openCount = 0; - const props = { - closeable: true, - open: false, - onRequestOpen() { - openCount++; - }, - title: 'test' - }; - - titlePane.setProperties(props); - titlePane.sendEvent('keyup', { - eventInit: { keyCode: Keys.Enter }, - selector: `.${css.title}` - }); - assert.strictEqual(openCount, 1, 'onRequestOpen should be called on title enter keyup'); - - titlePane.setProperties(props); - titlePane.sendEvent('keyup', { - eventInit: { keyCode: Keys.Space }, - selector: `.${css.title}` - }); - assert.strictEqual(openCount, 2, 'onRequestOpen should be called on title space keyup'); - }, - - 'close on keyup'() { - let closeCount = 0; - const props = { - closeable: true, - open: true, - onRequestClose() { - closeCount++; - }, - title: 'test' - }; - - titlePane.setProperties(props); - titlePane.sendEvent('keyup', { - eventInit: { keyCode: Keys.Enter }, - selector: `.${css.title}` - }); - assert.strictEqual(closeCount, 1, 'onRequestClose should be called on title enter keyup'); - - titlePane.setProperties(props); - titlePane.sendEvent('keyup', { - eventInit: { keyCode: Keys.Space }, - selector: `.${css.title}` - }); - assert.strictEqual(closeCount, 2, 'onRequestClose should be called on title space keyup'); - }, - - 'keyup: only respond to enter and space'() { - let called = false; - titlePane.setProperties({ - closeable: true, - open: false, - onRequestClose() { - called = true; - }, - onRequestOpen() { - called = true; - }, - title: 'test' - }); - - for (let i = 8; i < 223; i++) { - if (i !== Keys.Enter && i !== Keys.Space) { - titlePane.sendEvent('keyup', { - eventInit: { keyCode: i }, - selector: `.${css.title}` - }); - assert.isFalse(called, `keyCode {i} should be ignored`); - } - } } } });