-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New team setup UI #6666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
New team setup UI #6666
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
74bcbd2
deduplicate team role descriptions and extract role_picker
RobertJoonas 1953ae5
new team setup ui
RobertJoonas 0116bee
simplify form
RobertJoonas 40328b8
move add/remove rows and role picking interactions to FE
RobertJoonas 92ee57e
improve UX (keyboard nav, ARIA, close dropdown clicking outside)
RobertJoonas 4114b81
cleanup TeamSetup code
RobertJoonas 6ce7b9c
prevent blank name submit
RobertJoonas aaf9577
npm format
RobertJoonas 99d4841
prevent hitting team member limit on submit without prior warning
RobertJoonas 5d31cf9
fix CE compile warning
RobertJoonas 25bfc33
fix dialyzer and e2e tests
RobertJoonas 3aa2937
use force_change and avoid double-flagging name errors
RobertJoonas 66ec0d0
simplify hook comment + move add another button under the last email …
RobertJoonas be0984c
move team name update into transaction
RobertJoonas 5349ae6
Merge remote-tracking branch 'origin/master' into team-setup-form
RobertJoonas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| // Lets people add/remove member rows and pick a role instantly, without | ||
| // waiting on a server round trip - this is a JS hook rather than plain | ||
| // LiveView because that latency would be noticeable for something the | ||
| // server doesn't need to know about until the form is actually submitted. | ||
|
|
||
| const ROW_ID_PLACEHOLDER = '__ROW_ID__' | ||
|
|
||
| const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1) | ||
|
|
||
| export default { | ||
| mounted() { | ||
| this.template = this.el.querySelector('template[data-row-template]') | ||
| this.list = this.el.querySelector('[data-row-list]') | ||
| this.maxRows = parseInt(this.el.dataset.maxRows, 10) | ||
| this.addButton = this.el.querySelector('[data-add-row]') | ||
|
|
||
| this.addButton.addEventListener('click', () => this.addRow()) | ||
|
|
||
| this.list.addEventListener('click', (e) => { | ||
| const removeButton = e.target.closest('[data-remove-row]') | ||
| if (removeButton) return this.removeRow(removeButton) | ||
|
|
||
| const roleItem = e.target.closest('[data-role-item]') | ||
| if (roleItem) return this.selectRole(roleItem) | ||
| }) | ||
|
|
||
| // Native <details> only closes on a second click on <summary> - close it | ||
| // on an outside click too, like any other dropdown. | ||
| this.handleOutsideClick = (e) => { | ||
| this.list | ||
| .querySelectorAll('[data-role-picker][open]') | ||
| .forEach((details) => { | ||
| if (!details.contains(e.target)) this.closeRolePicker(details) | ||
| }) | ||
| } | ||
| document.addEventListener('click', this.handleOutsideClick) | ||
|
|
||
| this.list | ||
| .querySelectorAll('[data-role-picker]') | ||
| .forEach((details) => this.wireRolePicker(details)) | ||
|
|
||
| this.updateAddButtonState() | ||
| }, | ||
|
|
||
| destroyed() { | ||
| document.removeEventListener('click', this.handleOutsideClick) | ||
| }, | ||
|
|
||
| addRow() { | ||
| if (this.list.children.length >= this.maxRows) return | ||
|
|
||
| const rowId = | ||
| window.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}` | ||
|
|
||
| const html = this.template.innerHTML.replaceAll(ROW_ID_PLACEHOLDER, rowId) | ||
| const wrapper = document.createElement('div') | ||
| wrapper.innerHTML = html | ||
| const row = wrapper.firstElementChild | ||
|
|
||
| this.list.appendChild(row) | ||
| this.wireRolePicker(row.querySelector('[data-role-picker]')) | ||
| this.updateAddButtonState() | ||
| row.querySelector('input[type="email"]').focus() | ||
| }, | ||
|
|
||
| updateAddButtonState() { | ||
| const atLimit = this.list.children.length >= this.maxRows | ||
|
|
||
| this.addButton.classList.toggle('hidden', atLimit) | ||
| this.addButton.classList.toggle('inline-flex', !atLimit) | ||
| }, | ||
|
|
||
| removeRow(button) { | ||
| button.closest('[data-row]').remove() | ||
| this.updateAddButtonState() | ||
| }, | ||
|
|
||
| selectRole(item) { | ||
| const row = item.closest('[data-row]') | ||
| const details = row.querySelector('[data-role-picker]') | ||
| const items = [...details.querySelectorAll('[data-role-item]')] | ||
| const role = item.dataset.roleItem | ||
|
|
||
| row.querySelector('[data-role-value]').value = role | ||
| row.querySelector('[data-role-label]').textContent = capitalize(role) | ||
| items.forEach((i) => i.setAttribute('aria-selected', i === item)) | ||
|
|
||
| this.closeRolePicker(details) | ||
| details.querySelector('summary').focus() | ||
| }, | ||
|
|
||
| // Wires up the WAI-ARIA listbox-button keyboard pattern for a role picker: | ||
|
apata marked this conversation as resolved.
|
||
| // arrow keys move a roving tabindex between options (opening the listbox on | ||
| // first use if needed), Home/End jump to the ends, Escape closes and | ||
| // returns focus to the trigger, and Tab closes the listbox on its way out. | ||
| wireRolePicker(details) { | ||
| const summary = details.querySelector('summary') | ||
| const items = [...details.querySelectorAll('[data-role-item]')] | ||
|
|
||
| details.addEventListener('toggle', () => { | ||
| summary.setAttribute('aria-expanded', details.open) | ||
| this.setRovingIndex(items, 0) | ||
| }) | ||
|
|
||
| details.addEventListener('keydown', (e) => { | ||
| const currentIndex = items.indexOf(document.activeElement) | ||
|
|
||
| switch (e.key) { | ||
| case 'ArrowDown': | ||
| e.preventDefault() | ||
| details.open = true | ||
| this.focusItem( | ||
| items, | ||
| currentIndex === -1 ? 0 : (currentIndex + 1) % items.length | ||
| ) | ||
| break | ||
|
|
||
| case 'ArrowUp': | ||
| e.preventDefault() | ||
| details.open = true | ||
| this.focusItem( | ||
| items, | ||
| currentIndex === -1 | ||
| ? items.length - 1 | ||
| : (currentIndex - 1 + items.length) % items.length | ||
| ) | ||
| break | ||
|
|
||
| case 'Home': | ||
| if (!details.open) return | ||
| e.preventDefault() | ||
| this.focusItem(items, 0) | ||
| break | ||
|
|
||
| case 'End': | ||
| if (!details.open) return | ||
| e.preventDefault() | ||
| this.focusItem(items, items.length - 1) | ||
| break | ||
|
|
||
| case 'Escape': | ||
| if (!details.open) return | ||
| this.closeRolePicker(details) | ||
| summary.focus() | ||
| break | ||
|
|
||
| case 'Tab': | ||
| this.closeRolePicker(details) | ||
| break | ||
| } | ||
| }) | ||
| }, | ||
|
|
||
| focusItem(items, index) { | ||
| this.setRovingIndex(items, index) | ||
| items[index].focus() | ||
| }, | ||
|
|
||
| setRovingIndex(items, index) { | ||
| items.forEach((item, i) => | ||
| item.setAttribute('tabindex', i === index ? '0' : '-1') | ||
| ) | ||
| }, | ||
|
|
||
| closeRolePicker(details) { | ||
| details.removeAttribute('open') | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.