Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(FocusZone): Add embed mode for FocusZone and new Chat behavior #233

Merged
merged 21 commits into from
Sep 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e8bcdfa
FocusZone ref as a private function
tomasiser Sep 13, 2018
58e3bae
Embed FocusZone and Chat(Message) behaviors
tomasiser Sep 13, 2018
5620566
Fixed event propagation and ref for embed and key actions for chat
tomasiser Sep 13, 2018
127326e
Fixing embed FocusZone to conform to unit tests
tomasiser Sep 14, 2018
223a6d3
Update FocusZone changelog
tomasiser Sep 14, 2018
84f0180
Introduced handlesAccessibility tests to chat and fixed them for embed
tomasiser Sep 14, 2018
dcda42c
Changed focus zone to only search for default active element in desce…
tomasiser Sep 14, 2018
384521b
Fixes
tomasiser Sep 14, 2018
43ced1e
defaultTabbableElement
tomasiser Sep 19, 2018
473ef4b
PR comments fixed
tomasiser Sep 19, 2018
7bcd9b8
Merge remote-tracking branch 'origin/master' into feat/acc-focuszone-…
tomasiser Sep 20, 2018
d2931a6
Fixed order of ...rest to pass unit tests
tomasiser Sep 20, 2018
b9ac7a0
Fixed lint problems
tomasiser Sep 20, 2018
ea1935c
Merge branch 'master' into feat/acc-focuszone-embed
Sep 20, 2018
1da2d53
export chat behaviors
Sep 20, 2018
7256552
Merge remote-tracking branch 'origin/master' into feat/acc-focuszone-…
tomasiser Sep 25, 2018
b6cf5bb
Updated FocusZone changelog with correct PR
tomasiser Sep 25, 2018
8ce8f0c
Merge remote-tracking branch 'origin/feat/acc-focuszone-embed' into f…
tomasiser Sep 25, 2018
a36d931
Update changelog
tomasiser Sep 26, 2018
c97760c
Merge remote-tracking branch 'origin/master' into feat/acc-focuszone-…
tomasiser Sep 26, 2018
451b3cf
Update changelog
tomasiser Sep 26, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Features
- Add embed mode for `FocusZone` and use it in newly added Chat behaviors @tomasiser ([#233](https://github.com/stardust-ui/react/pull/233))

<!--------------------------------[ v0.7.0 ]------------------------------- -->
## [v0.7.0](https://github.com/stardust-ui/react/tree/v0.7.0) (2018-09-25)
[Compare changes](https://github.com/stardust-ui/react/compare/v0.6.0...v0.7.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ const messages = [
{ key: 2, content: 'Hi', author: 'Jane Doe', timestamp: 'Yesterday, 10:15 PM' },
{
key: 3,
content: "Let's go get some lunch!",
content: (
<>
Let's go get some lunch! I suggest the new <a>Downtown Burgers</a> or <a>Chef's Finest</a>.
</>
),
author: 'John Doe',
timestamp: 'Yesterday, 10:15 PM',
mine: true,
Expand Down
31 changes: 27 additions & 4 deletions src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { childrenExist, customPropTypes, UIComponent } from '../../lib'
import ChatMessage from './ChatMessage'
import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme'
import { Extendable, ReactChildren, ItemShorthand } from '../../../types/utils'
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/interfaces'
import ChatBehavior from '../../lib/accessibility/Behaviors/Chat/ChatBehavior'

export interface IChatProps {
accessibility?: Accessibility
as?: any
tomasiser marked this conversation as resolved.
Show resolved Hide resolved
className?: string
children?: ReactChildren
Expand All @@ -22,6 +25,9 @@ class Chat extends UIComponent<Extendable<IChatProps>, any> {
static displayName = 'Chat'

static propTypes = {
/** Accessibility behavior if overridden by the user. */
accessibility: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),

as: customPropTypes.as,

/** Additional CSS class name(s) to apply. */
Expand All @@ -39,17 +45,34 @@ class Chat extends UIComponent<Extendable<IChatProps>, any> {
variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
}

static handledProps = ['as', 'children', 'className', 'messages', 'styles', 'variables']
static handledProps = [
'accessibility',
'as',
'children',
'className',
'messages',
'styles',
'variables',
]

static defaultProps = { as: 'ul' }
static defaultProps = { accessibility: ChatBehavior as Accessibility, as: 'ul' }

static Message = ChatMessage

renderComponent({ ElementType, classes, rest }) {
actionHandlers: AccessibilityActionHandlers = {
focus: event => this.focusZone && this.focusZone.focus(),
}

renderComponent({ ElementType, classes, accessibility, rest }) {
const { children, messages } = this.props

return (
<ElementType {...rest} className={classes.root}>
<ElementType
className={classes.root}
{...accessibility.attributes.root}
{...accessibility.keyHandlers.root}
{...rest}
>
{childrenExist(children)
? children
: _.map(messages, message => ChatMessage.create(message))}
Expand Down
40 changes: 32 additions & 8 deletions src/components/Chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import {
} from '../../../types/theme'
import { Extendable, ReactChildren, ItemShorthand } from '../../../types/utils'
import Avatar from '../Avatar'
import ChatMessageBehavior from '../../lib/accessibility/Behaviors/Chat/ChatMessageBehavior'
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/interfaces'
import Layout from '../Layout'
import Text from '../Text'

export interface IChatMessageProps {
accessibility?: Accessibility
as?: any
tomasiser marked this conversation as resolved.
Show resolved Hide resolved
author?: ItemShorthand
avatar?: ItemShorthand
Expand All @@ -40,6 +43,9 @@ class ChatMessage extends UIComponent<Extendable<IChatMessageProps>, any> {
static displayName = 'ChatMessage'

static propTypes = {
/** Accessibility behavior if overridden by the user. */
accessibility: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),

as: customPropTypes.as,

/** Author of the message. */
Expand Down Expand Up @@ -71,6 +77,7 @@ class ChatMessage extends UIComponent<Extendable<IChatMessageProps>, any> {
}

static handledProps = [
'accessibility',
'as',
'author',
'avatar',
Expand All @@ -84,32 +91,49 @@ class ChatMessage extends UIComponent<Extendable<IChatMessageProps>, any> {
]

static defaultProps = {
accessibility: ChatMessageBehavior as Accessibility,
as: 'li',
}

actionHandlers: AccessibilityActionHandlers = {
// prevents default FocusZone behavior, e.g., in ChatMessageBehavior, it prevents FocusZone from using arrow keys as navigation (only Tab key should work)
preventDefault: event => {
event.preventDefault()
},
tomasiser marked this conversation as resolved.
Show resolved Hide resolved
}

renderComponent({
ElementType,
classes,
accessibility,
rest,
styles,
variables,
}: IRenderResultConfig<IChatMessageProps>) {
const { as, avatar, children, mine } = this.props
const { avatar, children, mine } = this.props

return childrenExist(children) ? (
<ElementType {...rest} className={cx(classes.root, classes.content)}>
{children}
</ElementType>
const childrenPropExists = childrenExist(children)
const className = childrenPropExists ? cx(classes.root, classes.content) : classes.root
const content = childrenPropExists ? (
children
) : (
<Layout
as={as}
{...rest}
className={classes.root}
start={!mine && this.renderAvatar(avatar, styles.avatar, variables)}
main={this.renderContent(classes.content, styles, variables)}
end={mine && this.renderAvatar(avatar, styles.avatar, variables)}
/>
)

return (
<ElementType
{...accessibility.attributes.root}
{...accessibility.keyHandlers.root}
{...rest}
className={className}
>
{content}
</ElementType>
)
}

private renderContent = (
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ export {
export {
default as RadioGroupItemBehavior,
} from './lib/accessibility/Behaviors/Radio/RadioGroupItemBehavior'
export { default as ChatBehavior } from './lib/accessibility/Behaviors/Chat/ChatBehavior'
export {
default as ChatMessageBehavior,
} from './lib/accessibility/Behaviors/Chat/ChatMessageBehavior'
export {
default as ChatBehaviorEnterEsc,
} from './lib/accessibility/Behaviors/Chat/ChatEnterEscBehavior'
export {
default as ChatMessageBehaviorEnterEsc,
} from './lib/accessibility/Behaviors/Chat/ChatMessageEnterEscBehavior'

export { default as Portal } from './components/Portal'
export { default as Popup } from './components/Popup'
export { PopupContent } from './components/Popup'
7 changes: 7 additions & 0 deletions src/lib/UIComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import renderComponent, { IRenderResultConfig } from './renderComponent'
import { AccessibilityActionHandlers } from './accessibility/interfaces'
import { IFocusZone } from './accessibility/FocusZone'

class UIComponent<P, S> extends React.Component<P, S> {
private readonly childClass = this.constructor as typeof UIComponent
Expand All @@ -9,6 +10,7 @@ class UIComponent<P, S> extends React.Component<P, S> {
static className: string
static handledProps: any
protected actionHandlers: AccessibilityActionHandlers
protected focusZone: IFocusZone

constructor(props, context) {
super(props, context)
Expand Down Expand Up @@ -38,10 +40,15 @@ class UIComponent<P, S> extends React.Component<P, S> {
props: this.props,
state: this.state,
actionHandlers: this.actionHandlers,
focusZoneRef: this.setFocusZoneRef,
},
this.renderComponent,
)
}

private setFocusZoneRef = (focusZone: IFocusZone): void => {
this.focusZone = focusZone
}
tomasiser marked this conversation as resolved.
Show resolved Hide resolved
}

export default UIComponent
37 changes: 37 additions & 0 deletions src/lib/accessibility/Behaviors/Chat/ChatBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Accessibility, FocusZoneMode } from '../../interfaces'
import * as keyboardKey from 'keyboard-key'
import { FocusZoneDirection } from '../../FocusZone'

const CHAT_FOCUSZONE_ATTRIBUTE = 'chat-focuszone'

/**
* @description
* Adds role 'presentation' until we come up with final roles for chat.
* Adds a vertical focus zone navigation with a last message as a default tabbable element, pressing right arrow key focuses inside a message.
* Adds a left arrow key action which focuses the chat, i.e., moves key handling from inside a message back to the chat list.
*/
const ChatBehavior: Accessibility = {
tomasiser marked this conversation as resolved.
Show resolved Hide resolved
attributes: {
root: {
role: 'presentation',
},
},
focusZone: {
mode: FocusZoneMode.Wrap,
props: {
shouldEnterInnerZone: event => keyboardKey.getCode(event) === keyboardKey.ArrowRight,
direction: FocusZoneDirection.vertical,
defaultTabbableElement: `[${CHAT_FOCUSZONE_ATTRIBUTE}] > * > *:last-child`, // select last chat message by default
[CHAT_FOCUSZONE_ATTRIBUTE]: '', // allows querying the default active element
},
},
keyActions: {
root: {
focus: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever used as a focus action? Or is it more something like regainFocus?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used when you want to unfocus a message and focus the whole list again, so Juraj and I think that focus is an appropriate action name as the only thing it does is it literally focuses the chat. :)

keyCombinations: [{ keyCode: keyboardKey.ArrowLeft }],
},
},
},
}

export default ChatBehavior
37 changes: 37 additions & 0 deletions src/lib/accessibility/Behaviors/Chat/ChatEnterEscBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Accessibility, FocusZoneMode } from '../../interfaces'
import * as keyboardKey from 'keyboard-key'
import { FocusZoneDirection } from '../../FocusZone'

const CHAT_FOCUSZONE_ATTRIBUTE = 'chat-focuszone'

/**
* @description
* Adds role 'presentation' until we come up with final roles for chat.
* Adds a vertical focus zone navigation with a last message as a default tabbable element, pressing enter key focuses inside a message.
* Adds an escape key action which focuses the chat, i.e., moves key handling from inside a message back to the chat list.
*/
const ChatEnterEscBehavior: Accessibility = {
attributes: {
root: {
role: 'presentation',
},
},
focusZone: {
mode: FocusZoneMode.Wrap,
props: {
shouldEnterInnerZone: event => keyboardKey.getCode(event) === keyboardKey.Enter,
direction: FocusZoneDirection.vertical,
defaultTabbableElement: `[${CHAT_FOCUSZONE_ATTRIBUTE}] > * > *:last-child`, // select last chat message by default
[CHAT_FOCUSZONE_ATTRIBUTE]: '', // allows querying the default active element
},
},
keyActions: {
root: {
focus: {
keyCombinations: [{ keyCode: keyboardKey.Escape }],
},
},
},
}

export default ChatEnterEscBehavior
38 changes: 38 additions & 0 deletions src/lib/accessibility/Behaviors/Chat/ChatMessageBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Accessibility, FocusZoneMode } from '../../interfaces'
import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities'
import * as keyboardKey from 'keyboard-key'
import { FocusZoneTabbableElements, FocusZoneDirection } from '../../FocusZone'

/**
* @description
* Adds role 'presentation' until we come up with final roles for chat.
* Sets the message to be a focusable element.
* Adds a vertical circular focus zone navigation where a user navigates using a Tab key.
* Adds a key action which prevents up and down arrow keys from navigating in FocusZone, we only want a Tab key to navigate.
*/
const ChatMessageBehavior: Accessibility = {
attributes: {
root: {
role: 'presentation',
[IS_FOCUSABLE_ATTRIBUTE]: true,
},
},
focusZone: {
mode: FocusZoneMode.Embed,
props: {
handleTabKey: FocusZoneTabbableElements.all,
isCircularNavigation: true,
direction: FocusZoneDirection.vertical,
},
},
keyActions: {
root: {
// prevents default FocusZone behavior, in this case, prevents using arrow keys as navigation (we only want a Tab key to navigate)
preventDefault: {
keyCombinations: [{ keyCode: keyboardKey.ArrowUp }, { keyCode: keyboardKey.ArrowDown }],
},
},
},
}

export default ChatMessageBehavior
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Accessibility, FocusZoneMode } from '../../interfaces'
import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities'

/**
* @description
* Adds role 'presentation' until we come up with final roles for chat.
* Sets the message to be a focusable element.
* Adds a default focus zone navigation where a user navigates using arrow keys in all directions.
*/
const ChatMessageEnterEscBehavior: Accessibility = {
attributes: {
root: {
role: 'presentation',
[IS_FOCUSABLE_ATTRIBUTE]: true,
},
},
focusZone: {
mode: FocusZoneMode.Embed,
props: {
isCircularNavigation: true,
},
},
}

export default ChatMessageEnterEscBehavior
7 changes: 7 additions & 0 deletions src/lib/accessibility/FocusZone/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This is a list of changes made to this Stardust copy of FocusZone in comparison with the original [Fabric FocusZone @ 0f567e05952c6b50c691df2fb72d100b5e525d9e](https://github.com/OfficeDev/office-ui-fabric-react/blob/0f567e05952c6b50c691df2fb72d100b5e525d9e/packages/office-ui-fabric-react/src/components/FocusZone/FocusZone.tsx).

### feat(FocusZone): Add embed mode for FocusZone and new Chat behavior [#233](https://github.com/stardust-ui/react/pull/233)
- Replaced `onFocusNotification` with a regular `onFocus` event callback to pass unit tests with embed.
- Replaced `ref={this.setRef}` with `this.setRef(this)` in `componentDidMount` to support functional components, which is needed to pass unit tests with embed.
- Renamed `defaultActiveElement` to `defaultTabbableElement` and changed behavior:
- Changed to query only descendants of the focus zone instead of the whole document, which enables to write simpler selectors. Note that we do not lose any functionality by this, because selecting elements outside of focus zone had no effect.
- Changed not to call `this.focus()` on component mount (this was causing issues e.g., in docsite, where every change in source code would refocus the mounted component). Instead, you can now use a new property `shouldFocusOnMount`.

### feat(FocusZone): Implement FocusZone into renderComponent [#116](https://github.com/stardust-ui/react/pull/116)
- Prettier and linting fixes, e.g., removing semicolons, removing underscores from private methods.
- Moved `IS_FOCUSABLE_ATTRIBUTE` and others to `focusUtilities.ts`.
Expand Down
Loading