Skip to content

Commit 5d70062

Browse files
committed
XWIKI-24755: XClass properties can only be reordered with a mouse in the class editor
* Added a reorderControls widget adding a move up and a move down button to each item of a list * Made both buttons move the item on click and on the up and down arrow keys * Announced the new position of the moved item in a live region * Announced that the item is already at the end of the list it is moving towards * Replaced a pending announcement when a new move is performed * Replaced the class editor move handle span with that widget * Removed the default border and padding of the move buttons * Left room for the focus indicator of the last property tool * Documented that the sortable keeps its default cancel option to protect the move buttons * Added the move up and move down translation keys * Deprecated the moveProperty.handle.label translation key * Added the property move actions and the move button getters to ClassEditPage * Started the property drag from the property title in movePropertyBefore * Added reorderPropertyWithoutDragging to EditClassIT
1 parent 24fed46 commit 5d70062

7 files changed

Lines changed: 512 additions & 26 deletions

File tree

xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/javascript.vm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
'xwiki-job-runner': "#removeJsSuffix($services.webjars.url('org.xwiki.platform:xwiki-platform-job-webjar', ""jobRunner${jsExtension}""))",
105105
'xwiki-locale-picker': $xwiki.getSkinFile('localePicker.js', true),
106106
'xwiki-meta': $xwiki.getSkinFile('js/xwiki/meta.js'),
107+
'xwiki-reorder-controls': $xwiki.getSkinFile('uicomponents/widgets/reorderControls.js'),
107108
'xwiki-selectize': $xwiki.getSkinFile('uicomponents/suggest/xwiki.selectize.js'),
108109
'xwiki-suggestAttachments-bundle': $xwiki.getSkinFile('uicomponents/suggest/suggestAttachments.js'),
109110
'xwiki-tree-finder': "#removeJsSuffix($services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', ""finder${jsExtension}""))",

xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/EditClassIT.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,72 @@ void reorderProperty(TestUtils setup, TestReference reference) throws Exception
212212
assertEquals(List.of("testA", "testC", "testB"), classEditPage.getProperties());
213213
}
214214

215+
@Test
216+
@Order(6)
217+
void reorderPropertyWithoutDragging(TestUtils setup, TestReference reference) throws Exception
218+
{
219+
setup.rest().savePage(reference, "Some content", "");
220+
ClassEditPage classEditPage = setup.editClass(reference);
221+
classEditPage.addProperty("testA", "Number");
222+
classEditPage.addProperty("testB", "Number");
223+
classEditPage.addProperty("testC", "Number");
224+
225+
// A property added without reloading the editor gets its move buttons too, and they move it right away.
226+
// The properties added before it keep exactly one pair of buttons, however many times the editor adds one.
227+
assertEquals(List.of("testA", "testB", "testC"), classEditPage.getProperties());
228+
assertEquals(2, classEditPage.getMovePropertyButtonCount("testA"));
229+
assertEquals(2, classEditPage.getMovePropertyButtonCount("testC"));
230+
classEditPage.movePropertyUp("testC");
231+
assertEquals(List.of("testA", "testC", "testB"), classEditPage.getProperties());
232+
classEditPage.movePropertyDown("testC");
233+
assertEquals(List.of("testA", "testB", "testC"), classEditPage.getProperties());
234+
235+
classEditPage.clickSaveAndView();
236+
237+
classEditPage = setup.editClass(reference);
238+
assertEquals(List.of("testA", "testB", "testC"), classEditPage.getProperties());
239+
240+
// Both move controls are buttons, and each one has an accessible name and a tooltip naming the property it
241+
// moves.
242+
assertEquals("button", classEditPage.getMovePropertyUpButton("testC").getTagName());
243+
assertEquals("move up", classEditPage.getMovePropertyUpButtonName("testC"));
244+
assertEquals("Move property testC up", classEditPage.getMovePropertyUpButtonTooltip("testC"));
245+
assertEquals("move down", classEditPage.getMovePropertyDownButtonName("testA"));
246+
assertEquals("Move property testA down", classEditPage.getMovePropertyDownButtonTooltip("testA"));
247+
248+
// A single click moves the property, and the button keeps the focus so that several moves can be performed in
249+
// a row.
250+
classEditPage.movePropertyUp("testC");
251+
assertEquals(List.of("testA", "testC", "testB"), classEditPage.getProperties());
252+
assertTrue(classEditPage.isMovePropertyUpButtonFocused("testC"));
253+
classEditPage.waitForReorderAnnouncement("Property moved to position 2 out of 3");
254+
classEditPage.movePropertyUp("testC");
255+
assertEquals(List.of("testC", "testA", "testB"), classEditPage.getProperties());
256+
257+
// The first property cannot be moved further up, and says so rather than staying silent.
258+
classEditPage.movePropertyUp("testC");
259+
assertEquals(List.of("testC", "testA", "testB"), classEditPage.getProperties());
260+
classEditPage.waitForReorderAnnouncement("Property already at the top of the list");
261+
262+
// The last property cannot be moved further down, and says so too.
263+
classEditPage.movePropertyDown("testB");
264+
assertEquals(List.of("testC", "testA", "testB"), classEditPage.getProperties());
265+
classEditPage.waitForReorderAnnouncement("Property already at the bottom of the list");
266+
267+
// The arrow keys move the property too, and both buttons answer both keys.
268+
classEditPage.movePropertyDownWithKeyboard("testC");
269+
assertEquals(List.of("testA", "testC", "testB"), classEditPage.getProperties());
270+
classEditPage.movePropertyUpWithKeyboard("testC");
271+
assertEquals(List.of("testC", "testA", "testB"), classEditPage.getProperties());
272+
classEditPage.movePropertyDownWithKeyboard("testC");
273+
assertEquals(List.of("testA", "testC", "testB"), classEditPage.getProperties());
274+
275+
classEditPage.clickSaveAndView();
276+
277+
classEditPage = setup.editClass(reference);
278+
assertEquals(List.of("testA", "testC", "testB"), classEditPage.getProperties());
279+
}
280+
215281
private DocumentReference getTestClassDocumentReference(TestReference reference)
216282
{
217283
return new DocumentReference("TestClass", reference.getLastSpaceReference());

xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1346,7 +1346,13 @@ core.editors.class.addProperty.inProgress=Adding property...
13461346
core.editors.class.addProperty.done=Property added
13471347
core.editors.class.addProperty.failed=Failed:
13481348

1349-
core.editors.class.moveProperty.handle.label=Drag and drop to change the order
1349+
core.editors.class.moveProperty.up.text=move up
1350+
core.editors.class.moveProperty.up.tooltip=Move property {0} up
1351+
core.editors.class.moveProperty.up.boundary=Property already at the top of the list
1352+
core.editors.class.moveProperty.down.text=move down
1353+
core.editors.class.moveProperty.down.tooltip=Move property {0} down
1354+
core.editors.class.moveProperty.down.boundary=Property already at the bottom of the list
1355+
core.editors.class.moveProperty.moved=Property moved to position {0} out of {1}
13501356

13511357
core.editors.class.deleteProperty.text=delete
13521358
core.editors.class.deleteProperty.tooltip=Delete property {0}
@@ -5732,6 +5738,14 @@ admin.pagerights.info=These rights apply on this page only.
57325738
admin.pagerights.infoNonTerminalDoc=They do not affect the {0}children{1}.
57335739
core.edit.autosave.every=every
57345740

5741+
#######################################
5742+
## until 18.8.0RC1
5743+
#######################################
5744+
## The move handle this key labeled was replaced by a move up button and a move down button, each carrying its own
5745+
## label and tooltip, so the replacements are core.editors.class.moveProperty.up.tooltip and the down equivalent.
5746+
#@deprecated core.editors.class.moveProperty.up.tooltip
5747+
core.editors.class.moveProperty.handle.label=Drag and drop to change the order
5748+
57355749
## Used to indicate where deprecated keys end
57365750
#@deprecatedend
57375751

xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-ui/src/main/java/org/xwiki/test/ui/po/editor/ClassEditPage.java

Lines changed: 187 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323

2424
import org.openqa.selenium.By;
25+
import org.openqa.selenium.Keys;
2526
import org.openqa.selenium.WebElement;
2627
import org.openqa.selenium.interactions.Actions;
2728
import org.openqa.selenium.support.FindBy;
@@ -139,11 +140,193 @@ public List<String> getProperties()
139140
*/
140141
public void movePropertyBefore(String propertyToMove, String propertyBefore)
141142
{
142-
WebElement moveToolSource = getDriver()
143-
.findElementWithoutWaiting(By.id("xproperty_" + propertyToMove))
144-
.findElement(By.cssSelector(".tool.move"));
145143
WebElement target = getDriver().findElementWithoutWaiting(By.id("xproperty_" + propertyBefore));
146-
new Actions(getDriver().getWrappedDriver()).dragAndDrop(moveToolSource, target).perform();
144+
new Actions(getDriver().getWrappedDriver()).dragAndDrop(getPropertyDragSource(propertyToMove), target)
145+
.perform();
146+
}
147+
148+
/**
149+
* Clicks the move up button of the given property. The property keeps its position when it is already the first
150+
* one.
151+
*
152+
* @param propertyToMove the property to move one position up
153+
* @since 18.8.0RC1
154+
*/
155+
public void movePropertyUp(String propertyToMove)
156+
{
157+
getMovePropertyUpButton(propertyToMove).click();
158+
}
159+
160+
/**
161+
* Clicks the move down button of the given property. The property keeps its position when it is already the last
162+
* one.
163+
*
164+
* @param propertyToMove the property to move one position down
165+
* @since 18.8.0RC1
166+
*/
167+
public void movePropertyDown(String propertyToMove)
168+
{
169+
getMovePropertyDownButton(propertyToMove).click();
170+
}
171+
172+
/**
173+
* Presses the up arrow key on the move buttons of the given property, which both answer it. The property keeps its
174+
* position when it is already the first one.
175+
*
176+
* @param propertyToMove the property to move one position up
177+
* @since 18.8.0RC1
178+
*/
179+
public void movePropertyUpWithKeyboard(String propertyToMove)
180+
{
181+
getMovePropertyUpButton(propertyToMove).sendKeys(Keys.ARROW_UP);
182+
}
183+
184+
/**
185+
* Presses the down arrow key on the move buttons of the given property, which both answer it. The property keeps
186+
* its position when it is already the last one.
187+
*
188+
* @param propertyToMove the property to move one position down
189+
* @since 18.8.0RC1
190+
*/
191+
public void movePropertyDownWithKeyboard(String propertyToMove)
192+
{
193+
getMovePropertyDownButton(propertyToMove).sendKeys(Keys.ARROW_DOWN);
194+
}
195+
196+
/**
197+
* @param propertyName the name of a property of this class
198+
* @return the button moving the given property one position up
199+
* @since 18.8.0RC1
200+
*/
201+
public WebElement getMovePropertyUpButton(String propertyName)
202+
{
203+
return getPropertyContainer(propertyName).findElement(By.cssSelector(".reorder-control-up"));
204+
}
205+
206+
/**
207+
* @param propertyName the name of a property of this class
208+
* @return the button moving the given property one position down
209+
* @since 18.8.0RC1
210+
*/
211+
public WebElement getMovePropertyDownButton(String propertyName)
212+
{
213+
return getPropertyContainer(propertyName).findElement(By.cssSelector(".reorder-control-down"));
214+
}
215+
216+
/**
217+
* @param propertyName the name of a property of this class
218+
* @return whether the button moving the given property up currently holds the focus
219+
* @since 18.8.0RC1
220+
*/
221+
public boolean isMovePropertyUpButtonFocused(String propertyName)
222+
{
223+
return getMovePropertyUpButton(propertyName).equals(getDriver().switchTo().activeElement());
224+
}
225+
226+
/**
227+
* @param propertyName the name of a property of this class
228+
* @return the accessible name of the button moving the given property up, which is displayed to screen readers
229+
* only
230+
* @since 18.8.0RC1
231+
*/
232+
public String getMovePropertyUpButtonName(String propertyName)
233+
{
234+
return getButtonName(getMovePropertyUpButton(propertyName));
235+
}
236+
237+
/**
238+
* @param propertyName the name of a property of this class
239+
* @return the accessible name of the button moving the given property down, which is displayed to screen readers
240+
* only
241+
* @since 18.8.0RC1
242+
*/
243+
public String getMovePropertyDownButtonName(String propertyName)
244+
{
245+
return getButtonName(getMovePropertyDownButton(propertyName));
246+
}
247+
248+
/**
249+
* @param propertyName the name of a property of this class
250+
* @return the tooltip of the button moving the given property up
251+
* @since 18.8.0RC1
252+
*/
253+
public String getMovePropertyUpButtonTooltip(String propertyName)
254+
{
255+
return getMovePropertyUpButton(propertyName).getDomAttribute("title");
256+
}
257+
258+
/**
259+
* @param propertyName the name of a property of this class
260+
* @return the tooltip of the button moving the given property down
261+
* @since 18.8.0RC1
262+
*/
263+
public String getMovePropertyDownButtonTooltip(String propertyName)
264+
{
265+
return getMovePropertyDownButton(propertyName).getDomAttribute("title");
266+
}
267+
268+
/**
269+
* @param propertyName the name of a property of this class
270+
* @return the number of move buttons displayed for the given property, which stays at two however many times
271+
* properties are added to the editor
272+
* @since 18.8.0RC1
273+
*/
274+
public int getMovePropertyButtonCount(String propertyName)
275+
{
276+
return getPropertyContainer(propertyName).findElements(By.cssSelector(".reorder-control")).size();
277+
}
278+
279+
/**
280+
* @param button one of the move buttons
281+
* @return the accessible name of the given button
282+
*/
283+
private String getButtonName(WebElement button)
284+
{
285+
return button.findElement(By.className("sr-only")).getDomProperty("textContent");
286+
}
287+
288+
/**
289+
* Waits until the move buttons have announced the given text to the screen readers.
290+
*
291+
* @param announcement the expected announcement
292+
* @since 18.8.0RC1
293+
*/
294+
public void waitForReorderAnnouncement(String announcement)
295+
{
296+
getDriver().waitUntilCondition(driver -> announcement.equals(getReorderAnnouncement()));
297+
}
298+
299+
/**
300+
* Reads the announcement without waiting for it. The live region is emptied as soon as a move is performed and
301+
* filled again shortly after, so this returns an empty string when it is called right after a move. Prefer
302+
* {@link #waitForReorderAnnouncement(String)}, which waits for the announcement to be set.
303+
*
304+
* @return the text currently held by the live region the move buttons announce their moves in
305+
* @since 18.8.0RC1
306+
*/
307+
public String getReorderAnnouncement()
308+
{
309+
return getDriver().findElementWithoutWaiting(By.id("reorder-controls-live-region"))
310+
.getDomProperty("textContent");
311+
}
312+
313+
/**
314+
* @param propertyName the name of a property of this class
315+
* @return the part of the property title a drag has to start from, the move buttons being operated by a click
316+
* rather than by a drag
317+
*/
318+
private WebElement getPropertyDragSource(String propertyName)
319+
{
320+
return getPropertyContainer(propertyName).findElement(By.cssSelector(".xproperty-title h2"));
321+
}
322+
323+
/**
324+
* @param propertyName the name of a property of this class
325+
* @return the element holding the title and the content of the given property
326+
*/
327+
private WebElement getPropertyContainer(String propertyName)
328+
{
329+
return getDriver().findElementWithoutWaiting(By.id("xproperty_" + propertyName));
147330
}
148331

149332
private FormContainerElement getForm()

xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-war/src/main/webapp/resources/js/xwiki/editors/dataeditors.css

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,10 @@ The default state is collapsed for xobjects and xproperties, but expanded for xc
223223
font-weight: 900;
224224
}
225225

226+
/* The right padding leaves room for the focus indicator of the last tool, which the hidden overflow would cut off. */
226227
.xproperty-title .tools {
227228
align-self: center;
229+
padding-right: 3px;
228230
}
229231

230232
.xproperty-title .tools .tool {
@@ -235,10 +237,16 @@ The default state is collapsed for xobjects and xproperties, but expanded for xc
235237
}
236238

237239
.xproperty-title .tools .move {
238-
cursor: move;
239240
color: $theme.linkColor;
240241
}
241242

243+
/* The move tools are buttons, so their default border and padding have to be removed to align them with the other
244+
tools. The background is already reset by the rule above. */
245+
.xproperty-title .tools button.tool {
246+
border: none;
247+
padding: 0;
248+
}
249+
242250
.xproperty-title .tools .delete {
243251
color: $theme.notificationErrorColor;
244252
}

0 commit comments

Comments
 (0)